CMS Project Sync
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SimplePie\HTTP;
|
||||
|
||||
/**
|
||||
* HTTP Client interface
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface Client
|
||||
{
|
||||
public const METHOD_GET = 'GET';
|
||||
|
||||
/**
|
||||
* send a request and return the response
|
||||
*
|
||||
* @param Client::METHOD_* $method
|
||||
* @param array<string, string> $headers
|
||||
*
|
||||
* @throws ClientException if anything goes wrong requesting the data
|
||||
*/
|
||||
public function request(string $method, string $url, array $headers = []): Response;
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SimplePie\HTTP;
|
||||
|
||||
use SimplePie\Exception as SimplePieException;
|
||||
|
||||
/**
|
||||
* Client exception class
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class ClientException extends SimplePieException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SimplePie\HTTP;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use SimplePie\File;
|
||||
use SimplePie\Misc;
|
||||
use SimplePie\Registry;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* HTTP Client based on \SimplePie\File
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class FileClient implements Client
|
||||
{
|
||||
/** @var Registry */
|
||||
private $registry;
|
||||
|
||||
/** @var array{timeout?: int, redirects?: int, useragent?: string, force_fsockopen?: bool, curl_options?: array<mixed>} */
|
||||
private $options;
|
||||
|
||||
/**
|
||||
* @param array{timeout?: int, redirects?: int, useragent?: string, force_fsockopen?: bool, curl_options?: array<mixed>} $options
|
||||
*/
|
||||
public function __construct(Registry $registry, array $options = [])
|
||||
{
|
||||
$this->registry = $registry;
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* send a request and return the response
|
||||
*
|
||||
* @param Client::METHOD_* $method
|
||||
* @param array<string, string> $headers
|
||||
*
|
||||
* @throws ClientException if anything goes wrong requesting the data
|
||||
*/
|
||||
public function request(string $method, string $url, array $headers = []): Response
|
||||
{
|
||||
// @phpstan-ignore-next-line Enforce PHPDoc type.
|
||||
if ($method !== self::METHOD_GET) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'%s(): Argument #1 ($method) only supports method "%s".',
|
||||
__METHOD__,
|
||||
self::METHOD_GET
|
||||
), 1);
|
||||
}
|
||||
|
||||
try {
|
||||
$file = $this->registry->create(File::class, [
|
||||
$url,
|
||||
$this->options['timeout'] ?? 10,
|
||||
$this->options['redirects'] ?? 5,
|
||||
$headers,
|
||||
$this->options['useragent'] ?? Misc::get_default_useragent(),
|
||||
$this->options['force_fsockopen'] ?? false,
|
||||
$this->options['curl_options'] ?? []
|
||||
]);
|
||||
} catch (Throwable $th) {
|
||||
throw new ClientException($th->getMessage(), $th->getCode(), $th);
|
||||
}
|
||||
|
||||
if ($file->error !== null && $file->get_status_code() === 0) {
|
||||
throw new ClientException($file->error);
|
||||
}
|
||||
|
||||
return $file;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SimplePie\HTTP;
|
||||
|
||||
/**
|
||||
* HTTP Response Parser
|
||||
* @template Psr7Compatible of bool
|
||||
*/
|
||||
class Parser
|
||||
{
|
||||
/**
|
||||
* HTTP Version
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
public $http_version = 0.0;
|
||||
|
||||
/**
|
||||
* Status code
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $status_code = 0;
|
||||
|
||||
/**
|
||||
* Reason phrase
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $reason = '';
|
||||
|
||||
/**
|
||||
* @var Psr7Compatible whether headers are compatible with PSR-7 format.
|
||||
*/
|
||||
private $psr7Compatible;
|
||||
|
||||
/**
|
||||
* Key/value pairs of the headers
|
||||
*
|
||||
* @var (Psr7Compatible is true ? array<string, non-empty-array<string>> : array<string, string>)
|
||||
*/
|
||||
public $headers = [];
|
||||
|
||||
/**
|
||||
* Body of the response
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $body = '';
|
||||
|
||||
private const STATE_HTTP_VERSION = 'http_version';
|
||||
|
||||
private const STATE_STATUS = 'status';
|
||||
|
||||
private const STATE_REASON = 'reason';
|
||||
|
||||
private const STATE_NEW_LINE = 'new_line';
|
||||
|
||||
private const STATE_BODY = 'body';
|
||||
|
||||
private const STATE_NAME = 'name';
|
||||
|
||||
private const STATE_VALUE = 'value';
|
||||
|
||||
private const STATE_VALUE_CHAR = 'value_char';
|
||||
|
||||
private const STATE_QUOTE = 'quote';
|
||||
|
||||
private const STATE_QUOTE_ESCAPED = 'quote_escaped';
|
||||
|
||||
private const STATE_QUOTE_CHAR = 'quote_char';
|
||||
|
||||
private const STATE_CHUNKED = 'chunked';
|
||||
|
||||
private const STATE_EMIT = 'emit';
|
||||
|
||||
private const STATE_ERROR = false;
|
||||
|
||||
/**
|
||||
* Current state of the state machine
|
||||
*
|
||||
* @var self::STATE_*
|
||||
*/
|
||||
protected $state = self::STATE_HTTP_VERSION;
|
||||
|
||||
/**
|
||||
* Input data
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $data = '';
|
||||
|
||||
/**
|
||||
* Input data length (to avoid calling strlen() everytime this is needed)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $data_length = 0;
|
||||
|
||||
/**
|
||||
* Current position of the pointer
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $position = 0;
|
||||
|
||||
/**
|
||||
* Name of the header currently being parsed
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name = '';
|
||||
|
||||
/**
|
||||
* Value of the header currently being parsed
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $value = '';
|
||||
|
||||
/**
|
||||
* Create an instance of the class with the input data
|
||||
*
|
||||
* @param string $data Input data
|
||||
* @param Psr7Compatible $psr7Compatible Whether the data types are in format compatible with PSR-7.
|
||||
*/
|
||||
public function __construct(string $data, bool $psr7Compatible = false)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->data_length = strlen($this->data);
|
||||
$this->psr7Compatible = $psr7Compatible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the input data
|
||||
*
|
||||
* @return bool true on success, false on failure
|
||||
*/
|
||||
public function parse()
|
||||
{
|
||||
while ($this->state && $this->state !== self::STATE_EMIT && $this->has_data()) {
|
||||
$state = $this->state;
|
||||
$this->$state();
|
||||
}
|
||||
$this->data = '';
|
||||
if ($this->state === self::STATE_EMIT || $this->state === self::STATE_BODY) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reset the parser state.
|
||||
$this->http_version = 0.0;
|
||||
$this->status_code = 0;
|
||||
$this->reason = '';
|
||||
$this->headers = [];
|
||||
$this->body = '';
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there is data beyond the pointer
|
||||
*
|
||||
* @return bool true if there is further data, false if not
|
||||
*/
|
||||
protected function has_data()
|
||||
{
|
||||
return (bool) ($this->position < $this->data_length);
|
||||
}
|
||||
|
||||
/**
|
||||
* See if the next character is LWS
|
||||
*
|
||||
* @return bool true if the next character is LWS, false if not
|
||||
*/
|
||||
protected function is_linear_whitespace()
|
||||
{
|
||||
return (bool) ($this->data[$this->position] === "\x09"
|
||||
|| $this->data[$this->position] === "\x20"
|
||||
|| ($this->data[$this->position] === "\x0A"
|
||||
&& isset($this->data[$this->position + 1])
|
||||
&& ($this->data[$this->position + 1] === "\x09" || $this->data[$this->position + 1] === "\x20")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the HTTP version
|
||||
* @return void
|
||||
*/
|
||||
protected function http_version()
|
||||
{
|
||||
if (strpos($this->data, "\x0A") !== false && strtoupper(substr($this->data, 0, 5)) === 'HTTP/') {
|
||||
$len = strspn($this->data, '0123456789.', 5);
|
||||
$http_version = substr($this->data, 5, $len);
|
||||
$this->position += 5 + $len;
|
||||
if (substr_count($http_version, '.') <= 1) {
|
||||
$this->http_version = (float) $http_version;
|
||||
$this->position += strspn($this->data, "\x09\x20", $this->position);
|
||||
$this->state = self::STATE_STATUS;
|
||||
} else {
|
||||
$this->state = self::STATE_ERROR;
|
||||
}
|
||||
} else {
|
||||
$this->state = self::STATE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the status code
|
||||
* @return void
|
||||
*/
|
||||
protected function status()
|
||||
{
|
||||
if ($len = strspn($this->data, '0123456789', $this->position)) {
|
||||
$this->status_code = (int) substr($this->data, $this->position, $len);
|
||||
$this->position += $len;
|
||||
$this->state = self::STATE_REASON;
|
||||
} else {
|
||||
$this->state = self::STATE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the reason phrase
|
||||
* @return void
|
||||
*/
|
||||
protected function reason()
|
||||
{
|
||||
$len = strcspn($this->data, "\x0A", $this->position);
|
||||
$this->reason = trim(substr($this->data, $this->position, $len), "\x09\x0D\x20");
|
||||
$this->position += $len + 1;
|
||||
$this->state = self::STATE_NEW_LINE;
|
||||
}
|
||||
|
||||
private function add_header(string $name, string $value): void
|
||||
{
|
||||
if ($this->psr7Compatible) {
|
||||
// For PHPStan: should be enforced by template parameter but PHPStan is not smart enough.
|
||||
/** @var array<string, non-empty-array<string>> */
|
||||
$headers = &$this->headers;
|
||||
$headers[$name][] = $value;
|
||||
} else {
|
||||
// For PHPStan: should be enforced by template parameter but PHPStan is not smart enough.
|
||||
/** @var array<string, string>) */
|
||||
$headers = &$this->headers;
|
||||
$headers[$name] .= ', ' . $value;
|
||||
}
|
||||
}
|
||||
|
||||
private function replace_header(string $name, string $value): void
|
||||
{
|
||||
if ($this->psr7Compatible) {
|
||||
// For PHPStan: should be enforced by template parameter but PHPStan is not smart enough.
|
||||
/** @var array<string, non-empty-array<string>> */
|
||||
$headers = &$this->headers;
|
||||
$headers[$name] = [$value];
|
||||
} else {
|
||||
// For PHPStan: should be enforced by template parameter but PHPStan is not smart enough.
|
||||
/** @var array<string, string>) */
|
||||
$headers = &$this->headers;
|
||||
$headers[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deal with a new line, shifting data around as needed
|
||||
* @return void
|
||||
*/
|
||||
protected function new_line()
|
||||
{
|
||||
$this->value = trim($this->value, "\x0D\x20");
|
||||
if ($this->name !== '' && $this->value !== '') {
|
||||
$this->name = strtolower($this->name);
|
||||
// We should only use the last Content-Type header. c.f. issue #1
|
||||
if (isset($this->headers[$this->name]) && $this->name !== 'content-type') {
|
||||
$this->add_header($this->name, $this->value);
|
||||
} else {
|
||||
$this->replace_header($this->name, $this->value);
|
||||
}
|
||||
}
|
||||
$this->name = '';
|
||||
$this->value = '';
|
||||
if (substr($this->data[$this->position], 0, 2) === "\x0D\x0A") {
|
||||
$this->position += 2;
|
||||
$this->state = self::STATE_BODY;
|
||||
} elseif ($this->data[$this->position] === "\x0A") {
|
||||
$this->position++;
|
||||
$this->state = self::STATE_BODY;
|
||||
} else {
|
||||
$this->state = self::STATE_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a header name
|
||||
* @return void
|
||||
*/
|
||||
protected function name()
|
||||
{
|
||||
$len = strcspn($this->data, "\x0A:", $this->position);
|
||||
if (isset($this->data[$this->position + $len])) {
|
||||
if ($this->data[$this->position + $len] === "\x0A") {
|
||||
$this->position += $len;
|
||||
$this->state = self::STATE_NEW_LINE;
|
||||
} else {
|
||||
$this->name = substr($this->data, $this->position, $len);
|
||||
$this->position += $len + 1;
|
||||
$this->state = self::STATE_VALUE;
|
||||
}
|
||||
} else {
|
||||
$this->state = self::STATE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse LWS, replacing consecutive LWS characters with a single space
|
||||
* @return void
|
||||
*/
|
||||
protected function linear_whitespace()
|
||||
{
|
||||
do {
|
||||
if (substr($this->data, $this->position, 2) === "\x0D\x0A") {
|
||||
$this->position += 2;
|
||||
} elseif ($this->data[$this->position] === "\x0A") {
|
||||
$this->position++;
|
||||
}
|
||||
$this->position += strspn($this->data, "\x09\x20", $this->position);
|
||||
} while ($this->has_data() && $this->is_linear_whitespace());
|
||||
$this->value .= "\x20";
|
||||
}
|
||||
|
||||
/**
|
||||
* See what state to move to while within non-quoted header values
|
||||
* @return void
|
||||
*/
|
||||
protected function value()
|
||||
{
|
||||
if ($this->is_linear_whitespace()) {
|
||||
$this->linear_whitespace();
|
||||
} else {
|
||||
switch ($this->data[$this->position]) {
|
||||
case '"':
|
||||
// Workaround for ETags: we have to include the quotes as
|
||||
// part of the tag.
|
||||
if (strtolower($this->name) === 'etag') {
|
||||
$this->value .= '"';
|
||||
$this->position++;
|
||||
$this->state = self::STATE_VALUE_CHAR;
|
||||
break;
|
||||
}
|
||||
$this->position++;
|
||||
$this->state = self::STATE_QUOTE;
|
||||
break;
|
||||
|
||||
case "\x0A":
|
||||
$this->position++;
|
||||
$this->state = self::STATE_NEW_LINE;
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->state = self::STATE_VALUE_CHAR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a header value while outside quotes
|
||||
* @return void
|
||||
*/
|
||||
protected function value_char()
|
||||
{
|
||||
$len = strcspn($this->data, "\x09\x20\x0A\"", $this->position);
|
||||
$this->value .= substr($this->data, $this->position, $len);
|
||||
$this->position += $len;
|
||||
$this->state = self::STATE_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* See what state to move to while within quoted header values
|
||||
* @return void
|
||||
*/
|
||||
protected function quote()
|
||||
{
|
||||
if ($this->is_linear_whitespace()) {
|
||||
$this->linear_whitespace();
|
||||
} else {
|
||||
switch ($this->data[$this->position]) {
|
||||
case '"':
|
||||
$this->position++;
|
||||
$this->state = self::STATE_VALUE;
|
||||
break;
|
||||
|
||||
case "\x0A":
|
||||
$this->position++;
|
||||
$this->state = self::STATE_NEW_LINE;
|
||||
break;
|
||||
|
||||
case '\\':
|
||||
$this->position++;
|
||||
$this->state = self::STATE_QUOTE_ESCAPED;
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->state = self::STATE_QUOTE_CHAR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a header value while within quotes
|
||||
* @return void
|
||||
*/
|
||||
protected function quote_char()
|
||||
{
|
||||
$len = strcspn($this->data, "\x09\x20\x0A\"\\", $this->position);
|
||||
$this->value .= substr($this->data, $this->position, $len);
|
||||
$this->position += $len;
|
||||
$this->state = self::STATE_VALUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an escaped character within quotes
|
||||
* @return void
|
||||
*/
|
||||
protected function quote_escaped()
|
||||
{
|
||||
$this->value .= $this->data[$this->position];
|
||||
$this->position++;
|
||||
$this->state = self::STATE_QUOTE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the body
|
||||
* @return void
|
||||
*/
|
||||
protected function body()
|
||||
{
|
||||
$this->body = substr($this->data, $this->position);
|
||||
if (!empty($this->headers['transfer-encoding'])) {
|
||||
unset($this->headers['transfer-encoding']);
|
||||
$this->state = self::STATE_CHUNKED;
|
||||
} else {
|
||||
$this->state = self::STATE_EMIT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parsed a "Transfer-Encoding: chunked" body
|
||||
* @return void
|
||||
*/
|
||||
protected function chunked()
|
||||
{
|
||||
if (!preg_match('/^([0-9a-f]+)[^\r\n]*\r\n/i', trim($this->body))) {
|
||||
$this->state = self::STATE_EMIT;
|
||||
return;
|
||||
}
|
||||
|
||||
$decoded = '';
|
||||
$encoded = $this->body;
|
||||
|
||||
while (true) {
|
||||
$is_chunked = (bool) preg_match('/^([0-9a-f]+)[^\r\n]*\r\n/i', $encoded, $matches);
|
||||
if (!$is_chunked) {
|
||||
// Looks like it's not chunked after all
|
||||
$this->state = self::STATE_EMIT;
|
||||
return;
|
||||
}
|
||||
|
||||
$length = hexdec(trim($matches[1]));
|
||||
// For PHPStan: this will only be float when larger than PHP_INT_MAX.
|
||||
// But even on 32-bit systems, it would mean 2GiB chunk, which sounds unlikely.
|
||||
\assert(\is_int($length), "Length needs to be shorter than PHP_INT_MAX");
|
||||
if ($length === 0) {
|
||||
// Ignore trailer headers
|
||||
$this->state = self::STATE_EMIT;
|
||||
$this->body = $decoded;
|
||||
return;
|
||||
}
|
||||
|
||||
$chunk_length = strlen($matches[0]);
|
||||
$decoded .= substr($encoded, $chunk_length, $length);
|
||||
$encoded = substr($encoded, $chunk_length + $length + 2);
|
||||
|
||||
// BC for PHP < 8.0: substr() can return bool instead of string
|
||||
$encoded = ($encoded === false) ? '' : $encoded;
|
||||
|
||||
if (trim($encoded) === '0' || empty($encoded)) {
|
||||
$this->state = self::STATE_EMIT;
|
||||
$this->body = $decoded;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare headers (take care of proxies headers)
|
||||
*
|
||||
* @param string $headers Raw headers
|
||||
* @param non-negative-int $count Redirection count. Default to 1.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function prepareHeaders(string $headers, int $count = 1)
|
||||
{
|
||||
$data = explode("\r\n\r\n", $headers, $count);
|
||||
$data = array_pop($data);
|
||||
if (false !== stripos($data, "HTTP/1.0 200 Connection established\r\n")) {
|
||||
$exploded = explode("\r\n\r\n", $data, 2);
|
||||
$data = end($exploded);
|
||||
}
|
||||
if (false !== stripos($data, "HTTP/1.1 200 Connection established\r\n")) {
|
||||
$exploded = explode("\r\n\r\n", $data, 2);
|
||||
$data = end($exploded);
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
class_alias('SimplePie\HTTP\Parser', 'SimplePie_HTTP_Parser');
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SimplePie\HTTP;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\RequestFactoryInterface;
|
||||
use Psr\Http\Message\UriFactoryInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* HTTP Client based on PSR-18 and PSR-17 implementations
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Psr18Client implements Client
|
||||
{
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $httpClient;
|
||||
|
||||
/**
|
||||
* @var RequestFactoryInterface
|
||||
*/
|
||||
private $requestFactory;
|
||||
|
||||
/**
|
||||
* @var UriFactoryInterface
|
||||
*/
|
||||
private $uriFactory;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $allowedRedirects = 5;
|
||||
|
||||
public function __construct(ClientInterface $httpClient, RequestFactoryInterface $requestFactory, UriFactoryInterface $uriFactory)
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
$this->requestFactory = $requestFactory;
|
||||
$this->uriFactory = $uriFactory;
|
||||
}
|
||||
|
||||
public function getHttpClient(): ClientInterface
|
||||
{
|
||||
return $this->httpClient;
|
||||
}
|
||||
|
||||
public function getRequestFactory(): RequestFactoryInterface
|
||||
{
|
||||
return $this->requestFactory;
|
||||
}
|
||||
|
||||
public function getUriFactory(): UriFactoryInterface
|
||||
{
|
||||
return $this->uriFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* send a request and return the response
|
||||
*
|
||||
* @param Client::METHOD_* $method
|
||||
* @param string $url
|
||||
* @param array<string,string|string[]> $headers
|
||||
*
|
||||
* @throws ClientException if anything goes wrong requesting the data
|
||||
*/
|
||||
public function request(string $method, string $url, array $headers = []): Response
|
||||
{
|
||||
if ($method !== self::METHOD_GET) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'%s(): Argument #1 ($method) only supports method "%s".',
|
||||
__METHOD__,
|
||||
self::METHOD_GET
|
||||
), 1);
|
||||
}
|
||||
|
||||
if (preg_match('/^http(s)?:\/\//i', $url)) {
|
||||
return $this->requestUrl($method, $url, $headers);
|
||||
}
|
||||
|
||||
return $this->requestLocalFile($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string|string[]> $headers
|
||||
*/
|
||||
private function requestUrl(string $method, string $url, array $headers): Response
|
||||
{
|
||||
$permanentUrl = $url;
|
||||
$requestedUrl = $url;
|
||||
$remainingRedirects = $this->allowedRedirects;
|
||||
|
||||
$request = $this->requestFactory->createRequest(
|
||||
$method,
|
||||
$this->uriFactory->createUri($requestedUrl)
|
||||
);
|
||||
|
||||
foreach ($headers as $name => $value) {
|
||||
$request = $request->withHeader($name, $value);
|
||||
}
|
||||
|
||||
do {
|
||||
$followRedirect = false;
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->sendRequest($request);
|
||||
} catch (ClientExceptionInterface $th) {
|
||||
throw new ClientException($th->getMessage(), $th->getCode(), $th);
|
||||
}
|
||||
|
||||
$statusCode = $response->getStatusCode();
|
||||
|
||||
// If we have a redirect
|
||||
if (in_array($statusCode, [300, 301, 302, 303, 307]) && $response->hasHeader('Location')) {
|
||||
// Prevent infinity redirect loops
|
||||
if ($remainingRedirects <= 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
$remainingRedirects--;
|
||||
$followRedirect = true;
|
||||
|
||||
$requestedUrl = $response->getHeaderLine('Location');
|
||||
|
||||
if ($statusCode === 301) {
|
||||
$permanentUrl = $requestedUrl;
|
||||
}
|
||||
|
||||
$request = $request->withUri($this->uriFactory->createUri($requestedUrl));
|
||||
}
|
||||
} while ($followRedirect);
|
||||
|
||||
return new Psr7Response($response, $permanentUrl, $requestedUrl);
|
||||
}
|
||||
|
||||
private function requestLocalFile(string $path): Response
|
||||
{
|
||||
if (!is_readable($path)) {
|
||||
throw new ClientException(sprintf('file "%s" is not readable', $path));
|
||||
}
|
||||
|
||||
try {
|
||||
$raw = file_get_contents($path);
|
||||
} catch (Throwable $th) {
|
||||
throw new ClientException($th->getMessage(), $th->getCode(), $th);
|
||||
}
|
||||
|
||||
if ($raw === false) {
|
||||
throw new ClientException('file_get_contents() could not read the file', 1);
|
||||
}
|
||||
|
||||
return new RawTextResponse($raw, $path);
|
||||
}
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SimplePie\HTTP;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* HTTP Response based on a PSR-7 response
|
||||
*
|
||||
* This interface must be interoperable with Psr\Http\Message\ResponseInterface
|
||||
* @see https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class Psr7Response implements Response
|
||||
{
|
||||
/**
|
||||
* @var ResponseInterface
|
||||
*/
|
||||
private $response;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $permanent_url;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $requested_url;
|
||||
|
||||
public function __construct(ResponseInterface $response, string $permanent_url, string $requested_url)
|
||||
{
|
||||
$this->response = $response;
|
||||
$this->permanent_url = $permanent_url;
|
||||
$this->requested_url = $requested_url;
|
||||
}
|
||||
|
||||
public function get_permanent_uri(): string
|
||||
{
|
||||
return $this->permanent_url;
|
||||
}
|
||||
|
||||
public function get_final_requested_uri(): string
|
||||
{
|
||||
return $this->requested_url;
|
||||
}
|
||||
|
||||
public function get_status_code(): int
|
||||
{
|
||||
return $this->response->getStatusCode();
|
||||
}
|
||||
|
||||
public function get_headers(): array
|
||||
{
|
||||
// The filtering is probably redundant but let’s make PHPStan happy.
|
||||
return array_filter($this->response->getHeaders(), function (array $header): bool {
|
||||
return count($header) >= 1;
|
||||
});
|
||||
}
|
||||
|
||||
public function has_header(string $name): bool
|
||||
{
|
||||
return $this->response->hasHeader($name);
|
||||
}
|
||||
|
||||
public function with_header(string $name, $value)
|
||||
{
|
||||
return new self($this->response->withHeader($name, $value), $this->permanent_url, $this->requested_url);
|
||||
}
|
||||
|
||||
public function get_header(string $name): array
|
||||
{
|
||||
return $this->response->getHeader($name);
|
||||
}
|
||||
|
||||
public function get_header_line(string $name): string
|
||||
{
|
||||
return $this->response->getHeaderLine($name);
|
||||
}
|
||||
|
||||
public function get_body_content(): string
|
||||
{
|
||||
return $this->response->getBody()->__toString();
|
||||
}
|
||||
}
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SimplePie\HTTP;
|
||||
|
||||
/**
|
||||
* HTTP Response for rax text
|
||||
*
|
||||
* This interface must be interoperable with Psr\Http\Message\ResponseInterface
|
||||
* @see https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class RawTextResponse implements Response
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $raw_text;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $permanent_url;
|
||||
|
||||
/**
|
||||
* @var array<non-empty-array<string>>
|
||||
*/
|
||||
private $headers = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $requested_url;
|
||||
|
||||
public function __construct(string $raw_text, string $filepath)
|
||||
{
|
||||
$this->raw_text = $raw_text;
|
||||
$this->permanent_url = $filepath;
|
||||
$this->requested_url = $filepath;
|
||||
}
|
||||
|
||||
public function get_permanent_uri(): string
|
||||
{
|
||||
return $this->permanent_url;
|
||||
}
|
||||
|
||||
public function get_final_requested_uri(): string
|
||||
{
|
||||
return $this->requested_url;
|
||||
}
|
||||
|
||||
public function get_status_code(): int
|
||||
{
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function get_headers(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
public function has_header(string $name): bool
|
||||
{
|
||||
return isset($this->headers[strtolower($name)]);
|
||||
}
|
||||
|
||||
public function get_header(string $name): array
|
||||
{
|
||||
return isset($this->headers[strtolower($name)]) ? $this->headers[$name] : [];
|
||||
}
|
||||
|
||||
public function with_header(string $name, $value)
|
||||
{
|
||||
$new = clone $this;
|
||||
|
||||
$newHeader = [
|
||||
strtolower($name) => (array) $value,
|
||||
];
|
||||
$new->headers = $newHeader + $this->headers;
|
||||
|
||||
return $new;
|
||||
}
|
||||
|
||||
public function get_header_line(string $name): string
|
||||
{
|
||||
return isset($this->headers[strtolower($name)]) ? implode(", ", $this->headers[$name]) : '';
|
||||
}
|
||||
|
||||
public function get_body_content(): string
|
||||
{
|
||||
return $this->raw_text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?php
|
||||
|
||||
// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
|
||||
// SPDX-FileCopyrightText: 2014 PHP Framework Interoperability Group
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SimplePie\HTTP;
|
||||
|
||||
/**
|
||||
* HTTP Response interface
|
||||
*
|
||||
* This interface must be interoperable with Psr\Http\Message\ResponseInterface
|
||||
* @see https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
interface Response
|
||||
{
|
||||
/**
|
||||
* Return the string representation of the permanent URI of the requested resource
|
||||
* (the first location after a prefix of (only) permanent redirects).
|
||||
*
|
||||
* Depending on which components of the URI are present, the resulting
|
||||
* string is either a full URI or relative reference according to RFC 3986,
|
||||
* Section 4.1. The method concatenates the various components of the URI,
|
||||
* using the appropriate delimiters:
|
||||
*
|
||||
* - If a scheme is present, it MUST be suffixed by ":".
|
||||
* - If an authority is present, it MUST be prefixed by "//".
|
||||
* - The path can be concatenated without delimiters. But there are two
|
||||
* cases where the path has to be adjusted to make the URI reference
|
||||
* valid as PHP does not allow to throw an exception in __toString():
|
||||
* - If the path is rootless and an authority is present, the path MUST
|
||||
* be prefixed by "/".
|
||||
* - If the path is starting with more than one "/" and no authority is
|
||||
* present, the starting slashes MUST be reduced to one.
|
||||
* - If a query is present, it MUST be prefixed by "?".
|
||||
* - If a fragment is present, it MUST be prefixed by "#".
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc3986#section-4.1
|
||||
*/
|
||||
public function get_permanent_uri(): string;
|
||||
|
||||
/**
|
||||
* Return the string representation of the final requested URL after following all redirects.
|
||||
*
|
||||
* Depending on which components of the URI are present, the resulting
|
||||
* string is either a full URI or relative reference according to RFC 3986,
|
||||
* Section 4.1. The method concatenates the various components of the URI,
|
||||
* using the appropriate delimiters:
|
||||
*
|
||||
* - If a scheme is present, it MUST be suffixed by ":".
|
||||
* - If an authority is present, it MUST be prefixed by "//".
|
||||
* - The path can be concatenated without delimiters. But there are two
|
||||
* cases where the path has to be adjusted to make the URI reference
|
||||
* valid as PHP does not allow to throw an exception in __toString():
|
||||
* - If the path is rootless and an authority is present, the path MUST
|
||||
* be prefixed by "/".
|
||||
* - If the path is starting with more than one "/" and no authority is
|
||||
* present, the starting slashes MUST be reduced to one.
|
||||
* - If a query is present, it MUST be prefixed by "?".
|
||||
* - If a fragment is present, it MUST be prefixed by "#".
|
||||
*
|
||||
* @see http://tools.ietf.org/html/rfc3986#section-4.1
|
||||
*/
|
||||
public function get_final_requested_uri(): string;
|
||||
|
||||
/**
|
||||
* Gets the response status code.
|
||||
*
|
||||
* The status code is a 3-digit integer result code of the server's attempt
|
||||
* to understand and satisfy the request.
|
||||
*
|
||||
* @return int Status code.
|
||||
*/
|
||||
public function get_status_code(): int;
|
||||
|
||||
/**
|
||||
* Retrieves all message header values.
|
||||
*
|
||||
* The keys represent the header name as it will be sent over the wire, and
|
||||
* each value is an array of strings associated with the header.
|
||||
*
|
||||
* // Represent the headers as a string
|
||||
* foreach ($message->get_headers() as $name => $values) {
|
||||
* echo $name . ': ' . implode(', ', $values);
|
||||
* }
|
||||
*
|
||||
* // Emit headers iteratively:
|
||||
* foreach ($message->get_headers() as $name => $values) {
|
||||
* foreach ($values as $value) {
|
||||
* header(sprintf('%s: %s', $name, $value), false);
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @return array<non-empty-array<string>> Returns an associative array of the message's headers.
|
||||
* Each key MUST be a header name, and each value MUST be an array of
|
||||
* strings for that header.
|
||||
*/
|
||||
public function get_headers(): array;
|
||||
|
||||
/**
|
||||
* Checks if a header exists by the given case-insensitive name.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return bool Returns true if any header names match the given header
|
||||
* name using a case-insensitive string comparison. Returns false if
|
||||
* no matching header name is found in the message.
|
||||
*/
|
||||
public function has_header(string $name): bool;
|
||||
|
||||
/**
|
||||
* Retrieves a message header value by the given case-insensitive name.
|
||||
*
|
||||
* This method returns an array of all the header values of the given
|
||||
* case-insensitive header name.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return an
|
||||
* empty array.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string[] An array of string values as provided for the given
|
||||
* header. If the header does not appear in the message, this method MUST
|
||||
* return an empty array.
|
||||
*/
|
||||
public function get_header(string $name): array;
|
||||
|
||||
/**
|
||||
* Return an instance with the provided value replacing the specified header.
|
||||
*
|
||||
* This method MUST be implemented in such a way as to retain the
|
||||
* immutability of the message, and MUST return an instance that has the
|
||||
* new and/or updated header and value.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @param string|non-empty-array<string> $value Header value(s).
|
||||
* @return static
|
||||
* @throws \InvalidArgumentException for invalid header names or values.
|
||||
*/
|
||||
public function with_header(string $name, $value);
|
||||
|
||||
/**
|
||||
* Retrieves a comma-separated string of the values for a single header.
|
||||
*
|
||||
* This method returns all of the header values of the given
|
||||
* case-insensitive header name as a string concatenated together using
|
||||
* a comma.
|
||||
*
|
||||
* NOTE: Not all header values may be appropriately represented using
|
||||
* comma concatenation. For such headers, use getHeader() instead
|
||||
* and supply your own delimiter when concatenating.
|
||||
*
|
||||
* If the header does not appear in the message, this method MUST return
|
||||
* an empty string.
|
||||
*
|
||||
* @param string $name Case-insensitive header field name.
|
||||
* @return string A string of values as provided for the given header
|
||||
* concatenated together using a comma. If the header does not appear in
|
||||
* the message, this method MUST return an empty string.
|
||||
*/
|
||||
public function get_header_line(string $name): string;
|
||||
|
||||
/**
|
||||
* get the body as string
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_body_content(): string;
|
||||
}
|
||||
Reference in New Issue
Block a user