primo commit
This commit is contained in:
139
libraries/fof40/Download/Adapter/AbstractAdapter.php
Normal file
139
libraries/fof40/Download/Adapter/AbstractAdapter.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?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;
|
||||
|
||||
abstract class AbstractAdapter implements DownloadInterface
|
||||
{
|
||||
/**
|
||||
* Load order priority
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $priority = 100;
|
||||
|
||||
/**
|
||||
* Name of the adapter (identical to filename)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $name = '';
|
||||
|
||||
/**
|
||||
* Is this adapter supported in the current execution environment?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $isSupported = false;
|
||||
|
||||
/**
|
||||
* Does this adapter support chunked downloads?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $supportsChunkDownload = false;
|
||||
|
||||
/**
|
||||
* Does this adapter support querying the remote file's size?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $supportsFileSize = false;
|
||||
|
||||
/**
|
||||
* Does this download adapter support downloading files in chunks?
|
||||
*
|
||||
* @return boolean True if chunk download is supported
|
||||
*/
|
||||
public function supportsChunkDownload(): bool
|
||||
{
|
||||
return $this->supportsChunkDownload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does this download adapter support reading the size of a remote file?
|
||||
*
|
||||
* @return boolean True if remote file size determination is supported
|
||||
*/
|
||||
public function supportsFileSize(): bool
|
||||
{
|
||||
return $this->supportsFileSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this download class supported in the current server environment?
|
||||
*
|
||||
* @return boolean True if this server environment supports this download class
|
||||
*/
|
||||
public function isSupported(): bool
|
||||
{
|
||||
return $this->isSupported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the priority of this adapter. If multiple download adapters are
|
||||
* supported on a site, the one with the highest priority will be
|
||||
* used.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return $this->priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this download adapter in use
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
278
libraries/fof40/Download/Adapter/Curl.php
Normal file
278
libraries/fof40/Download/Adapter/Curl.php
Normal file
@ -0,0 +1,278 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
164
libraries/fof40/Download/Adapter/Fopen.php
Normal file
164
libraries/fof40/Download/Adapter/Fopen.php
Normal file
@ -0,0 +1,164 @@
|
||||
<?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 URL fopen() wrappers
|
||||
*/
|
||||
class Fopen extends AbstractAdapter implements DownloadInterface
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->priority = 100;
|
||||
$this->supportsFileSize = false;
|
||||
$this->supportsChunkDownload = true;
|
||||
$this->name = 'fopen';
|
||||
|
||||
$this->isSupported = !function_exists('ini_get') ? false : ini_get('allow_url_fopen');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
if (empty($from))
|
||||
{
|
||||
$from = 0;
|
||||
}
|
||||
|
||||
if (empty($to))
|
||||
{
|
||||
$to = 0;
|
||||
}
|
||||
|
||||
if ($to < $from)
|
||||
{
|
||||
$temp = $to;
|
||||
$to = $from;
|
||||
$from = $temp;
|
||||
unset($temp);
|
||||
}
|
||||
|
||||
|
||||
if (!(empty($from) && empty($to)))
|
||||
{
|
||||
$caCertPath = class_exists('\\Composer\\CaBundle\\CaBundle')
|
||||
? \Composer\CaBundle\CaBundle::getBundledCaBundlePath()
|
||||
: JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem';
|
||||
|
||||
$options = [
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
'header' => "Range: bytes=$from-$to\r\n",
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'cafile' => $caCertPath,
|
||||
'verify_depth' => 5,
|
||||
],
|
||||
];
|
||||
|
||||
$options = array_merge($options, $params);
|
||||
|
||||
$context = stream_context_create($options);
|
||||
$result = @file_get_contents($url, false, $context, $from - $to + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
$caCertPath = class_exists('\\Composer\\CaBundle\\CaBundle')
|
||||
? \Composer\CaBundle\CaBundle::getBundledCaBundlePath()
|
||||
: JPATH_LIBRARIES . '/src/Http/Transport/cacert.pem';
|
||||
|
||||
$options = [
|
||||
'http' => [
|
||||
'method' => 'GET',
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'cafile' => $caCertPath,
|
||||
'verify_depth' => 5,
|
||||
],
|
||||
];
|
||||
|
||||
$options = array_merge($options, $params);
|
||||
|
||||
$context = stream_context_create($options);
|
||||
$result = @file_get_contents($url, false, $context);
|
||||
}
|
||||
|
||||
global $http_response_header_test;
|
||||
|
||||
if (!isset($http_response_header) && empty($http_response_header_test))
|
||||
{
|
||||
$error = Text::_('LIB_FOF40_DOWNLOAD_ERR_FOPEN_ERROR');
|
||||
throw new DownloadError($error, 404);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Used for testing
|
||||
if (!isset($http_response_header) && !empty($http_response_header_test))
|
||||
{
|
||||
$http_response_header = $http_response_header_test;
|
||||
}
|
||||
|
||||
$http_code = 200;
|
||||
$nLines = count($http_response_header);
|
||||
|
||||
for ($i = $nLines - 1; $i >= 0; $i--)
|
||||
{
|
||||
$line = $http_response_header[$i];
|
||||
if (strncasecmp("HTTP", $line, 4) == 0)
|
||||
{
|
||||
$response = explode(' ', $line);
|
||||
$http_code = $response[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($http_code >= 299)
|
||||
{
|
||||
$error = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_HTTPERROR', $http_code);
|
||||
throw new DownloadError($error, $http_code);
|
||||
}
|
||||
}
|
||||
|
||||
if ($result === false)
|
||||
{
|
||||
$error = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_FOPEN_ERROR');
|
||||
throw new DownloadError($error, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
}
|
||||
501
libraries/fof40/Download/Download.php
Normal file
501
libraries/fof40/Download/Download.php
Normal file
@ -0,0 +1,501 @@
|
||||
<?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;
|
||||
|
||||
defined('_JEXEC') || die;
|
||||
|
||||
use FOF40\Container\Container;
|
||||
use FOF40\Download\Exception\DownloadError;
|
||||
use FOF40\Timer\Timer;
|
||||
use Joomla\CMS\Language\Text;
|
||||
|
||||
class Download
|
||||
{
|
||||
/**
|
||||
* The component container object
|
||||
*
|
||||
* @var Container
|
||||
*/
|
||||
protected $container;
|
||||
|
||||
/**
|
||||
* Parameters passed from the GUI when importing from URL
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $params = [];
|
||||
|
||||
/**
|
||||
* The download adapter which will be used by this class
|
||||
*
|
||||
* @var DownloadInterface
|
||||
*/
|
||||
private $adapter;
|
||||
|
||||
/**
|
||||
* Additional params that will be passed to the adapter while performing the download
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $adapterOptions = [];
|
||||
|
||||
/**
|
||||
* Public constructor
|
||||
*
|
||||
* @param Container $c The component container
|
||||
*/
|
||||
public function __construct(Container $c)
|
||||
{
|
||||
$this->container = $c;
|
||||
|
||||
// Find the best fitting adapter
|
||||
$allAdapters = self::getFiles(__DIR__ . '/Adapter', [], ['AbstractAdapter.php']);
|
||||
$priority = 0;
|
||||
|
||||
foreach ($allAdapters as $adapterInfo)
|
||||
{
|
||||
/** @var Adapter\AbstractAdapter $adapter */
|
||||
$adapter = new $adapterInfo['classname'];
|
||||
|
||||
if (!$adapter->isSupported())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($adapter->priority > $priority)
|
||||
{
|
||||
$this->adapter = $adapter;
|
||||
$priority = $adapter->priority;
|
||||
}
|
||||
}
|
||||
|
||||
// Load the language strings
|
||||
$c->platform->loadTranslations('lib_fof40');
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will crawl a starting directory and get all the valid files
|
||||
* that will be analyzed by __construct. Then it organizes them into an
|
||||
* associative array.
|
||||
*
|
||||
* @param string $path Folder where we should start looking
|
||||
* @param array $ignoreFolders Folder ignore list
|
||||
* @param array $ignoreFiles File ignore list
|
||||
*
|
||||
* @return array Associative array, where the `fullpath` key contains the path to the file,
|
||||
* and the `classname` key contains the name of the class
|
||||
*/
|
||||
protected static function getFiles(string $path, array $ignoreFolders = [], array $ignoreFiles = []): array
|
||||
{
|
||||
$return = [];
|
||||
|
||||
$files = self::scanDirectory($path, $ignoreFolders, $ignoreFiles);
|
||||
|
||||
// Ok, I got the files, now I have to organize them
|
||||
foreach ($files as $file)
|
||||
{
|
||||
$clean = str_replace($path, '', $file);
|
||||
$clean = trim(str_replace('\\', '/', $clean), '/');
|
||||
|
||||
$parts = explode('/', $clean);
|
||||
|
||||
$return[] = [
|
||||
'fullpath' => $file,
|
||||
'classname' => '\\FOF40\\Download\\Adapter\\' . ucfirst(basename($parts[0], '.php')),
|
||||
];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive function that will scan every directory unless it's in the
|
||||
* ignore list. Files that aren't in the ignore list are returned.
|
||||
*
|
||||
* @param string $path Folder where we should start looking
|
||||
* @param array $ignoreFolders Folder ignore list
|
||||
* @param array $ignoreFiles File ignore list
|
||||
*
|
||||
* @return array List of all the files
|
||||
*/
|
||||
protected static function scanDirectory(string $path, array $ignoreFolders = [], array $ignoreFiles = []): array
|
||||
{
|
||||
$return = [];
|
||||
|
||||
$handle = @opendir($path);
|
||||
|
||||
if (!$handle)
|
||||
{
|
||||
return $return;
|
||||
}
|
||||
|
||||
while (($file = readdir($handle)) !== false)
|
||||
{
|
||||
if ($file == '.' || $file == '..')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$fullpath = $path . '/' . $file;
|
||||
|
||||
if ((is_dir($fullpath) && in_array($file, $ignoreFolders)) || (is_file($fullpath) && in_array($file, $ignoreFiles)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_dir($fullpath))
|
||||
{
|
||||
$return = array_merge(self::scanDirectory($fullpath, $ignoreFolders, $ignoreFiles), $return);
|
||||
}
|
||||
else
|
||||
{
|
||||
$return[] = $path . '/' . $file;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the use of a specific adapter
|
||||
*
|
||||
* @param string $className The name of the class or the name of the adapter
|
||||
*/
|
||||
public function setAdapter(?string $className = null): void
|
||||
{
|
||||
if (is_null($className))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$adapter = null;
|
||||
|
||||
if (class_exists($className, true))
|
||||
{
|
||||
$adapter = new $className;
|
||||
}
|
||||
elseif (class_exists('\\FOF40\\Download\\Adapter\\' . ucfirst($className)))
|
||||
{
|
||||
$className = '\\FOF40\\Download\\Adapter\\' . ucfirst($className);
|
||||
$adapter = new $className;
|
||||
}
|
||||
|
||||
if (!is_object($adapter))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$adapter instanceof DownloadInterface)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->adapter = $adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the current adapter
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAdapterName(): string
|
||||
{
|
||||
if (is_object($this->adapter))
|
||||
{
|
||||
$class = get_class($this->adapter);
|
||||
|
||||
return strtolower(str_ireplace('FOF40\\Download\\Adapter\\', '', $class));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the additional options for the adapter
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function getAdapterOptions(): array
|
||||
{
|
||||
return $this->adapterOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the additional options for the adapter
|
||||
*
|
||||
* @param array $options
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function setAdapterOptions(array $options): void
|
||||
{
|
||||
$this->adapterOptions = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download data from a URL and return it.
|
||||
*
|
||||
* Important note about ranges: byte ranges start at 0. This means that the first 500 bytes of a file are from 0
|
||||
* to 499, NOT from 1 to 500. If you ask more bytes than there are in the file or a range which is invalid or does
|
||||
* not exist this method will return false.
|
||||
*
|
||||
* @param string $url The URL to download from
|
||||
* @param int $from Byte range to start downloading from. Use null (default) for start of file.
|
||||
* @param int $to Byte range to stop downloading. Use null to download the entire file ($from will be
|
||||
* ignored!)
|
||||
*
|
||||
* @return string The downloaded data or null on failure
|
||||
*/
|
||||
public function getFromURL(string $url, ?int $from = null, ?int $to = null): ?string
|
||||
{
|
||||
try
|
||||
{
|
||||
return $this->adapter->downloadAndReturn($url, $from, $to, $this->adapterOptions);
|
||||
}
|
||||
catch (DownloadError $e)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the staggered download of file.
|
||||
*
|
||||
* @param array $params A parameters array, as sent by the user interface
|
||||
*
|
||||
* @return array A return status array
|
||||
*/
|
||||
public function importFromURL(array $params): array
|
||||
{
|
||||
$this->params = $params;
|
||||
|
||||
// Fetch data
|
||||
$url = $this->getParam('url');
|
||||
$localFilename = $this->getParam('localFilename');
|
||||
$frag = $this->getParam('frag', -1);
|
||||
$totalSize = $this->getParam('totalSize', -1);
|
||||
$doneSize = $this->getParam('doneSize', -1);
|
||||
$maxExecTime = $this->getParam('maxExecTime', 5);
|
||||
$runTimeBias = $this->getParam('runTimeBias', 75);
|
||||
$length = $this->getParam('length', 1048576);
|
||||
|
||||
if (empty($localFilename))
|
||||
{
|
||||
$localFilename = basename($url);
|
||||
|
||||
if (strpos($localFilename, '?') !== false)
|
||||
{
|
||||
$paramsPos = strpos($localFilename, '?');
|
||||
$localFilename = substr($localFilename, 0, $paramsPos - 1);
|
||||
|
||||
$platformBaseDirectories = $this->container->platform->getPlatformBaseDirs();
|
||||
$tmpDir = $platformBaseDirectories['tmp'];
|
||||
$tmpDir = rtrim($tmpDir, '/\\');
|
||||
|
||||
$localFilename = $tmpDir . '/' . $localFilename;
|
||||
}
|
||||
}
|
||||
|
||||
// Init retArray
|
||||
$retArray = [
|
||||
"status" => true,
|
||||
"error" => '',
|
||||
"frag" => $frag,
|
||||
"totalSize" => $totalSize,
|
||||
"doneSize" => $doneSize,
|
||||
"percent" => 0,
|
||||
"localfile" => $localFilename,
|
||||
];
|
||||
|
||||
try
|
||||
{
|
||||
$timer = new Timer($maxExecTime, $runTimeBias);
|
||||
$start = $timer->getRunningTime(); // Mark the start of this download
|
||||
$break = false; // Don't break the step
|
||||
|
||||
do
|
||||
{
|
||||
// Do we have to initialize the file?
|
||||
if ($frag == -1)
|
||||
{
|
||||
// Currently downloaded size
|
||||
$doneSize = 0;
|
||||
|
||||
if (@file_exists($localFilename))
|
||||
{
|
||||
@unlink($localFilename);
|
||||
}
|
||||
|
||||
// Delete and touch the output file
|
||||
$fp = @fopen($localFilename, 'w');
|
||||
|
||||
if ($fp !== false)
|
||||
{
|
||||
@fclose($fp);
|
||||
}
|
||||
|
||||
// Init
|
||||
$frag = 0;
|
||||
|
||||
$retArray['totalSize'] = $this->adapter->getFileSize($url);
|
||||
|
||||
if ($retArray['totalSize'] <= 0)
|
||||
{
|
||||
$retArray['totalSize'] = 0;
|
||||
}
|
||||
|
||||
$totalSize = $retArray['totalSize'];
|
||||
}
|
||||
|
||||
// Calculate from and length
|
||||
$from = $frag * $length;
|
||||
$to = $length + $from - 1;
|
||||
|
||||
// Try to download the first frag
|
||||
$required_time = 1.0;
|
||||
|
||||
$error = '';
|
||||
|
||||
try
|
||||
{
|
||||
$result = $this->adapter->downloadAndReturn($url, $from, $to, $this->adapterOptions);
|
||||
}
|
||||
catch (DownloadError $e)
|
||||
{
|
||||
$result = false;
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
|
||||
if ($result === false)
|
||||
{
|
||||
// Failed download
|
||||
if ($frag == 0)
|
||||
{
|
||||
// Failure to download first frag = failure to download. Period.
|
||||
$retArray['status'] = false;
|
||||
$retArray['error'] = $error;
|
||||
|
||||
return $retArray;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Since this is a staggered download, consider this normal and finish
|
||||
$frag = -1;
|
||||
$totalSize = $doneSize;
|
||||
$break = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Add the currently downloaded frag to the total size of downloaded files
|
||||
if ($result !== false)
|
||||
{
|
||||
$fileSize = strlen($result);
|
||||
$doneSize += $fileSize;
|
||||
|
||||
// Append the file
|
||||
$fp = @fopen($localFilename, 'a');
|
||||
|
||||
if ($fp === false)
|
||||
{
|
||||
// Can't open the file for writing
|
||||
$retArray['status'] = false;
|
||||
$retArray['error'] = Text::sprintf('LIB_FOF40_DOWNLOAD_ERR_COULDNOTWRITELOCALFILE', $localFilename);
|
||||
|
||||
return $retArray;
|
||||
}
|
||||
|
||||
fwrite($fp, $result);
|
||||
fclose($fp);
|
||||
|
||||
$frag++;
|
||||
|
||||
if (($fileSize < $length) || ($fileSize > $length)
|
||||
|| (($totalSize == $doneSize) && ($totalSize > 0))
|
||||
)
|
||||
{
|
||||
// A partial download or a download larger than the frag size means we are done
|
||||
$frag = -1;
|
||||
//debugMsg("-- Import complete (partial download of last frag)");
|
||||
$totalSize = $doneSize;
|
||||
$break = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Advance the frag pointer and mark the end
|
||||
$end = $timer->getRunningTime();
|
||||
|
||||
// Do we predict that we have enough time?
|
||||
$required_time = max(1.1 * ($end - $start), $required_time);
|
||||
|
||||
if ($required_time > (10 - $end + $start))
|
||||
{
|
||||
$break = true;
|
||||
}
|
||||
|
||||
$start = $end;
|
||||
|
||||
} while (($timer->getTimeLeft() > 0) && !$break);
|
||||
|
||||
if ($frag == -1)
|
||||
{
|
||||
$percent = 100;
|
||||
}
|
||||
elseif ($doneSize <= 0)
|
||||
{
|
||||
$percent = 0;
|
||||
}
|
||||
elseif ($totalSize > 0)
|
||||
{
|
||||
$percent = 100 * ($doneSize / $totalSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
$percent = 0;
|
||||
}
|
||||
|
||||
// Update $retArray
|
||||
$retArray = [
|
||||
"status" => true,
|
||||
"error" => '',
|
||||
"frag" => $frag,
|
||||
"totalSize" => $totalSize,
|
||||
"doneSize" => $doneSize,
|
||||
"percent" => $percent,
|
||||
];
|
||||
}
|
||||
catch (DownloadError $e)
|
||||
{
|
||||
$retArray['status'] = false;
|
||||
$retArray['error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
return $retArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to decode the $params array
|
||||
*
|
||||
* @param string $key The parameter key you want to retrieve the value for
|
||||
* @param mixed $default The default value, if none is specified
|
||||
*
|
||||
* @return mixed The value for this parameter key
|
||||
*/
|
||||
private function getParam(string $key, $default = null)
|
||||
{
|
||||
if (array_key_exists($key, $this->params))
|
||||
{
|
||||
return $this->params[$key];
|
||||
}
|
||||
else
|
||||
{
|
||||
return $default;
|
||||
}
|
||||
}
|
||||
}
|
||||
87
libraries/fof40/Download/DownloadInterface.php
Normal file
87
libraries/fof40/Download/DownloadInterface.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?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;
|
||||
|
||||
defined('_JEXEC') || die;
|
||||
|
||||
use FOF40\Download\Exception\DownloadError;
|
||||
|
||||
/**
|
||||
* Interface DownloadInterface
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
interface DownloadInterface
|
||||
{
|
||||
/**
|
||||
* Does this download adapter support downloading files in chunks?
|
||||
*
|
||||
* @return boolean True if chunk download is supported
|
||||
*/
|
||||
public function supportsChunkDownload(): bool;
|
||||
|
||||
/**
|
||||
* Does this download adapter support reading the size of a remote file?
|
||||
*
|
||||
* @return boolean True if remote file size determination is supported
|
||||
*/
|
||||
public function supportsFileSize(): bool;
|
||||
|
||||
/**
|
||||
* Is this download class supported in the current server environment?
|
||||
*
|
||||
* @return boolean True if this server environment supports this download class
|
||||
*/
|
||||
public function isSupported(): bool;
|
||||
|
||||
/**
|
||||
* Get the priority of this adapter. If multiple download adapters are
|
||||
* supported on a site, the one with the highest priority will be
|
||||
* used.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
|
||||
/**
|
||||
* Returns the name of this download adapter in use
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* 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 int|null $from Byte range to start downloading from. Use null for start of file.
|
||||
* @param int|null $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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
17
libraries/fof40/Download/Exception/DownloadError.php
Normal file
17
libraries/fof40/Download/Exception/DownloadError.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?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\Exception;
|
||||
|
||||
defined('_JEXEC') || die;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class DownloadError extends RuntimeException
|
||||
{
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user