<?php
namespace ParagonIE\Certainty;
use GuzzleHttp\Client;
use ParagonIE\Certainty\Exception\EncodingException;
use ParagonIE\Certainty\Exception\NetworkException;
/**
* Class RemoteFetch
*
* Fetches data over the network. Caches locally.
*
* @package ParagonIE\Certainty
*/
class RemoteFetch extends Fetch
{
const CHECK_SIGNATURE_BY_DEFAULT = true;
const CHECK_CHRONICLE_BY_DEFAULT = true;
const DEFAULT_URL = 'https://raw.githubusercontent.com/paragonie/certainty/master/data/';
/**
* @var \DateInterval $cacheTimeout
*/
protected $cacheTimeout;
/**
* @var Client $http
*/
protected $http;
/**
* @var string $url
*/
protected $url = '';
/**
* RemoteFetch constructor.
*
* @param string $dataDir
* @param string $url
* @param Client|null $http
* @param \DateInterval|string|null $timeout
* @throws \TypeError
*/
public function __construct(
$dataDir = '',
$url = self::DEFAULT_URL,
Client $http = null,
$timeout = null
) {
parent::__construct($dataDir);
$this->url = $url;
if (\is_null($http)) {
if (\file_exists($this->dataDirectory . '/ca-certs.json')) {
$http = Certainty::getGuzzleClient(new Fetch($this->dataDirectory));
} else {
$http = new Client();
}
}
/** @var Client $http */
$this->http = $http;
if (\is_null($timeout)) {
/* Default: 24 hours */
$timeoutObj = new \DateInterval('P01D');
} elseif (\is_string($timeout)) {
$timeoutObj = new \DateInterval($timeout);
} elseif ($timeout instanceof \DateInterval) {
$timeoutObj = $timeout;
} else {
throw new \TypeError('Invalid timeout. Expected a DateInterval or string.');
}
/** @var \DateInterval $timeoutObj */
$this->cacheTimeout = $timeoutObj;
}
/**
* Do we need to fetch updates?
*
* @return bool
*/
public function cacheExpired()
{
if (!\file_exists($this->dataDirectory . '/ca-certs.cache')) {
return true;
}
/** @var string $cacheTime */
$cacheTime = \file_get_contents($this->dataDirectory . '/ca-certs.cache');
if (!\is_string($cacheTime)) {
return true;
}
$expires = (new \DateTime($cacheTime))->add($this->cacheTimeout);
return $expires <= new \DateTime('NOW');
}
/**
* List bundles
*
* @param string $customValidator
* @return array<int, Bundle>
* @throws NetworkException
*/
protected function listBundles($customValidator = '')
{
if ($this->cacheExpired()) {
if (!$this->remoteFetchBundles()) {
throw new NetworkException('Could not download bundles');
}
}
return parent::listBundles($customValidator);
}
/**
* This handles the actual HTTP request.
*
* @return bool
* @throws EncodingException
*/
protected function remoteFetchBundles()
{
$request = $this->http->get($this->url . '/ca-certs.json');
$body = (string) $request->getBody();
$jsonDecoded = \json_decode($body, true);
if (!\is_array($jsonDecoded)) {
throw new EncodingException(\json_last_error_msg());
}
if (\file_exists($this->dataDirectory . '/ca-certs.json')) {
\rename(
$this->dataDirectory . '/ca-certs.json',
$this->dataDirectory . '/ca-certs-backup-' . \date('YmdHis') . '.json'
);
}
\file_put_contents($this->dataDirectory . '/ca-certs.json', $body);
foreach ($jsonDecoded as $item) {
if (!isset($item['file'])) {
continue;
}
$filename = $item['file'];
if (!\preg_match('#^cacert(\-[0-9]{4}\-[0-9]{2}\-[0-9]{2})?\.pem$#', $filename)) {
// Invalid filename
continue;
}
if (!\file_exists($this->dataDirectory . '/' . $filename)) {
$request = $this->http->get($this->url . '/' . $filename);
$body = (string) $request->getBody();
\file_put_contents($this->dataDirectory . '/' . $filename, $body);
}
}
return !\is_bool(
\file_put_contents(
$this->dataDirectory . '/ca-certs.cache',
(new \DateTime())->format(\DateTime::ATOM)
)
);
}
/**
* @param \DateInterval $interval
* @return self
*/
public function setCacheTimeout(\DateInterval $interval)
{
$this->cacheTimeout = $interval;
return $this;
}
/**
* Replace the HTTP client with a new one.
*
* @param Client $client
* @return $this
*/
public function setHttpClient(Client $client)
{
$this->http = $client;
return $this;
}
/**
* @param string $url
* @return self
*/
public function setRemoteSource($url = '')
{
$this->url = $url;
return $this;
}
}
|