diff --git a/app/views/collection/edit.php b/app/views/collection/edit.php
index 71bffc41..6b7af08f 100644
--- a/app/views/collection/edit.php
+++ b/app/views/collection/edit.php
@@ -13,7 +13,7 @@
- = $helper->img($item['cover_image']); ?>
+ = $helper->img($urlGenerator->assetUrl("images/anime/{$item['hummingbird_id']}.jpg")); ?>
|
diff --git a/build/CodeIgniter/Sniffs/Strings/DoubleQuoteUsageSniff.php b/build/CodeIgniter/Sniffs/Strings/DoubleQuoteUsageSniff.php
index 419d3b14..d3eab4c7 100755
--- a/build/CodeIgniter/Sniffs/Strings/DoubleQuoteUsageSniff.php
+++ b/build/CodeIgniter/Sniffs/Strings/DoubleQuoteUsageSniff.php
@@ -393,12 +393,12 @@ class DoubleQuoteUsageSniff extends VariableUsageSniff
&& false === $smpl_qt_at
) {
$error = 'Single-quoted strings should be used unless it contains variables, special chars like \n or single quotes.';
- $phpcsFile->addError($error, $stackPtr);
+ $phpcsFile->addError($error, $stackPtr, 111);
} else if (false !== $smpl_qt_at && false !== $dbl_qt_at
&& false === $has_variable && false === $has_specific_sequence
) {
$warning = 'It is encouraged to use a single-quoted string, since it doesn\'t contain any variable nor special char though it mixes single and double quotes.';
- $phpcsFile->addWarning($warning, $stackPtr);
+ $phpcsFile->addWarning($warning, $stackPtr, 222);
}
}//end processDoubleQuotedString()
@@ -426,7 +426,7 @@ class DoubleQuoteUsageSniff extends VariableUsageSniff
$smpl_qt_at = strpos($qtString, "'");
if (false === $has_variable && false !== $smpl_qt_at && false === $dbl_qt_at) {
$warning = 'You may also use double-quoted strings if the string contains single quotes, so you do not have to use escape characters.';
- $phpcsFile->addWarning($warning, $stackPtr);
+ $phpcsFile->addWarning($warning, $stackPtr, 333);
}
}//end processSingleQuotedString()
diff --git a/composer.json b/composer.json
index 1bebffb8..7f13f0ca 100644
--- a/composer.json
+++ b/composer.json
@@ -17,7 +17,7 @@
}
},
"require": {
- "amphp/artax": "^2.0",
+ "amphp/artax": "^3.0",
"aura/html": "^2.0",
"aura/router": "^3.0",
"aura/session": "^2.0",
@@ -42,7 +42,9 @@
"sebastian/phpcpd": "^3.0",
"spatie/phpunit-snapshot-assertions": "^1.2.0",
"squizlabs/php_codesniffer": "^3.0.0@beta",
- "theseer/phpdox": "^0.10.1"
+ "symfony/var-dumper": "^4.0.1",
+ "theseer/phpdox": "^0.10.1",
+ "filp/whoops": "^2.1"
},
"scripts": {
"build": "vendor/bin/robo build",
diff --git a/index.php b/index.php
index cd54fb08..d59043cf 100644
--- a/index.php
+++ b/index.php
@@ -28,6 +28,10 @@ if ($timezone === '' || $timezone === FALSE)
// Load composer autoloader
require_once __DIR__ . '/vendor/autoload.php';
+$whoops = new \Whoops\Run;
+$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
+$whoops->register();
+
// Define base directories
$APP_DIR = _dir(__DIR__, 'app');
$APPCONF_DIR = _dir($APP_DIR, 'appConf');
diff --git a/public/js.php b/public/js.php
index 305d8863..cc9eaed6 100644
--- a/public/js.php
+++ b/public/js.php
@@ -16,8 +16,9 @@
namespace Aviat\EasyMin;
-use function Amp\wait;
-use Amp\Artax\{Client, FormBody, Request};
+use function Amp\Promise\wait;
+use Amp\Artax\Request;
+use Aviat\AnimeClient\API\HummingbirdClient;
use Aviat\Ion\{Json, JsonException};
// Include Amp and Artax
@@ -113,24 +114,24 @@ class JSMin {
* Makes a call to google closure compiler service
*
* @param array $options - Form parameters
+ * @throws \TypeError
* @return object
*/
protected function closureCall(array $options)
{
$formFields = http_build_query($options);
- $request = (new Request)
- ->setMethod('POST')
- ->setUri('https://closure-compiler.appspot.com/compile')
- ->setAllHeaders([
+ $request = (new Request('https://closure-compiler.appspot.com/compile'))
+ ->withMethod('POST')
+ ->withHeaders([
'Accept' => 'application/json',
'Accept-Encoding' => 'gzip',
'Content-type' => 'application/x-www-form-urlencoded'
])
- ->setBody($formFields);
+ ->withBody($formFields);
- $response = wait((new Client)->request($request, [
- Client::OP_AUTO_ENCODING => false
+ $response = wait((new HummingbirdClient)->request($request, [
+ HummingbirdClient::OP_AUTO_ENCODING => false
]));
return $response;
@@ -147,7 +148,7 @@ class JSMin {
try
{
$errorRes = $this->closureCall($options);
- $errorJson = $errorRes->getBody();
+ $errorJson = wait($errorRes->getBody());
$errorObj = Json::decode($errorJson) ?: (object)[];
@@ -237,7 +238,7 @@ class JSMin {
// Now actually retrieve the compiled code
$options['output_info'] = 'compiled_code';
$res = $this->closureCall($options);
- $json = $res->getBody();
+ $json = wait($res->getBody());
$obj = Json::decode($json);
//return $obj;
diff --git a/src/API/APIRequestBuilder.php b/src/API/APIRequestBuilder.php
index 7b85405f..17b91060 100644
--- a/src/API/APIRequestBuilder.php
+++ b/src/API/APIRequestBuilder.php
@@ -16,6 +16,8 @@
namespace Aviat\AnimeClient\API;
+use function Amp\Promise\wait;
+
use Amp;
use Amp\Artax\{FormBody, Request};
use Aviat\Ion\Json;
@@ -23,7 +25,7 @@ use InvalidArgumentException;
use Psr\Log\LoggerAwareTrait;
/**
- * Wrapper around Artex to make it easier to build API requests
+ * Wrapper around Artax to make it easier to build API requests
*/
class APIRequestBuilder {
use LoggerAwareTrait;
@@ -60,7 +62,7 @@ class APIRequestBuilder {
/**
* The current request
- * @var \Amp\Promise
+ * @var \Amp\Artax\Request
*/
protected $request;
@@ -96,11 +98,12 @@ class APIRequestBuilder {
* Set the request body
*
* @param FormBody|string $body
+ * @throws \TypeError
* @return self
*/
public function setBody($body): self
{
- $this->request->setBody($body);
+ $this->request = $this->request->withBody($body);
return $this;
}
@@ -108,13 +111,26 @@ class APIRequestBuilder {
* Set body as form fields
*
* @param array $fields Mapping of field names to values
+ * @throws \TypeError
* @return self
*/
public function setFormFields(array $fields): self
{
- $this->setHeader("Content-Type", "application/x-www-form-urlencoded");
- $body = (new FormBody)->addFields($fields);
- $this->setBody($body);
+ $body = new FormBody();
+ $body->addFields($fields);
+
+ return $this->setBody($body);
+ }
+
+ /**
+ * Unset a request header
+ *
+ * @param string $name
+ * @return self
+ */
+ public function unsetHeader(string $name): self
+ {
+ $this->request = $this->request->withoutHeader($name);
return $this;
}
@@ -125,9 +141,17 @@ class APIRequestBuilder {
* @param string $value
* @return self
*/
- public function setHeader(string $name, string $value): self
+ public function setHeader(string $name, string $value = NULL): self
{
- $this->request->setHeader($name, $value);
+ if (NULL === $value)
+ {
+ $this->unsetHeader($name);
+ }
+ else
+ {
+ $this->request = $this->request->withHeader($name, $value);
+ }
+
return $this;
}
@@ -153,6 +177,7 @@ class APIRequestBuilder {
* Set the request body
*
* @param mixed $body
+ * @throws \TypeError
* @return self
*/
public function setJsonBody($body): self
@@ -160,7 +185,7 @@ class APIRequestBuilder {
$requestBody = ( ! is_scalar($body))
? Json::encode($body)
: $body;
-
+
return $this->setBody($requestBody);
}
@@ -179,9 +204,10 @@ class APIRequestBuilder {
/**
* Return the promise for the current request
*
- * @return \Amp\Promise
+ * @throws \Throwable
+ * @return \Amp\Artax\Request
*/
- public function getFullRequest()
+ public function getFullRequest(): Request
{
$this->buildUri();
@@ -189,8 +215,12 @@ class APIRequestBuilder {
{
$this->logger->debug('API Request', [
'request_url' => $this->request->getUri(),
- 'request_headers' => $this->request->getAllHeaders(),
- 'request_body' => $this->request->getBody()
+ 'request_headers' => $this->request->getHeaders(),
+ 'request_body' => wait(
+ $this->request->getBody()
+ ->createBodyStream()
+ ->read()
+ )
]);
}
@@ -207,19 +237,21 @@ class APIRequestBuilder {
*/
public function newRequest(string $type, string $uri): self
{
- if ( ! in_array($type, $this->validMethods))
+ if ( ! \in_array($type, $this->validMethods, TRUE))
{
- throw new InvalidArgumentException('Invalid HTTP methods');
+ throw new InvalidArgumentException('Invalid HTTP method');
}
- $this->resetState();
-
- $this->request
- ->setMethod($type)
- ->setProtocol('1.1');
+ $realUrl = (strpos($uri, '//') !== FALSE)
+ ? $uri
+ : $this->baseUrl . $uri;
+ $this->resetState($realUrl, $type);
$this->path = $uri;
+ // Actually create the full url!
+ $this->buildUri();
+
if ( ! empty($this->defaultHeaders))
{
$this->setHeaders($this->defaultHeaders);
@@ -231,9 +263,9 @@ class APIRequestBuilder {
/**
* Create the full request url
*
- * @return void
+ * @return Request
*/
- private function buildUri()
+ private function buildUri(): Request
{
$url = (strpos($this->path, '//') !== FALSE)
? $this->path
@@ -244,18 +276,25 @@ class APIRequestBuilder {
$url .= '?' . $this->query;
}
- $this->request->setUri($url);
+ $this->request = $this->request->withUri($url);
+
+ return $this->request;
}
/**
* Reset the class state for a new request
*
+ * @param string $url
+ * @param string $type
* @return void
*/
- private function resetState()
+ private function resetState($url, $type = 'GET')
{
+ $requestUrl = $url ?: $this->baseUrl;
+
$this->path = '';
$this->query = '';
- $this->request = new Request();
+ $this->request = (new Request($requestUrl))
+ ->withMethod($type);
}
}
\ No newline at end of file
diff --git a/src/API/HummingbirdClient.php b/src/API/HummingbirdClient.php
new file mode 100644
index 00000000..01b8b8f1
--- /dev/null
+++ b/src/API/HummingbirdClient.php
@@ -0,0 +1,1155 @@
+ true,
+ self::OP_TRANSFER_TIMEOUT => 15000,
+ self::OP_MAX_REDIRECTS => 5,
+ self::OP_AUTO_REFERER => true,
+ self::OP_DISCARD_BODY => false,
+ self::OP_DEFAULT_HEADERS => [],
+ self::OP_MAX_HEADER_BYTES => Parser::DEFAULT_MAX_HEADER_BYTES,
+ self::OP_MAX_BODY_BYTES => Parser::DEFAULT_MAX_BODY_BYTES,
+ ];
+
+ public function __construct(
+ CookieJar $cookieJar = null,
+ HttpSocketPool $socketPool = null,
+ ClientTlsContext $tlsContext = null
+ )
+ {
+ $this->cookieJar = $cookieJar ?? new NullCookieJar;
+ $this->tlsContext = $tlsContext ?? new ClientTlsContext;
+ $this->socketPool = $socketPool ?? new HttpSocketPool;
+ $this->hasZlib = extension_loaded('zlib');
+ }
+
+ /** @inheritdoc */
+ public function request($uriOrRequest, array $options = [], CancellationToken $cancellation = null): Promise
+ {
+ return call(function () use ($uriOrRequest, $options, $cancellation) {
+ $cancellation = $cancellation ?? new NullCancellationToken;
+
+ foreach ($options as $option => $value) {
+ $this->validateOption($option, $value);
+ }
+
+ /** @var Request $request */
+ list($request, $uri) = $this->generateRequestFromUri($uriOrRequest);
+ $options = $options ? array_merge($this->options, $options) : $this->options;
+
+ foreach ($this->options[self::OP_DEFAULT_HEADERS] as $name => $header) {
+ if (!$request->hasHeader($name)) {
+ $request = $request->withHeaders([$name => $header]);
+ }
+ }
+
+ /** @var array $headers */
+ $headers = yield $request->getBody()->getHeaders();
+ foreach ($headers as $name => $header) {
+ if (!$request->hasHeader($name)) {
+ $request = $request->withHeaders([$name => $header]);
+ }
+ }
+
+ $originalUri = $uri;
+ $previousResponse = null;
+
+ $maxRedirects = $options[self::OP_MAX_REDIRECTS];
+ $requestNr = 1;
+
+ do {
+ /** @var Request $request */
+ $request = yield from $this->normalizeRequestBodyHeaders($request);
+ $request = $this->normalizeRequestHeaders($request, $uri, $options);
+
+ // Always normalize this as last item, because we need to strip sensitive headers
+ $request = $this->normalizeTraceRequest($request);
+
+ /** @var Response $response */
+ $response = yield $this->doRequest($request, $uri, $options, $previousResponse, $cancellation);
+
+ // Explicit $maxRedirects !== 0 check to not consume redirect bodies if redirect following is disabled
+ if ($maxRedirects !== 0 && $redirectUri = $this->getRedirectUri($response)) {
+ // Discard response body of redirect responses
+ $body = $response->getBody();
+ while (null !== yield $body->read()) ;
+
+ /**
+ * If this is a 302/303 we need to follow the location with a GET if the original request wasn't
+ * GET. Otherwise we need to send the body again.
+ *
+ * We won't resend the body nor any headers on redirects to other hosts for security reasons.
+ *
+ * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.3
+ */
+ $method = $request->getMethod();
+ $status = $response->getStatus();
+ $isSameHost = $redirectUri->getAuthority(false) === $originalUri->getAuthority(false);
+
+ if ($isSameHost) {
+ $request = $request->withUri($redirectUri);
+
+ if ($status >= 300 && $status <= 303 && $method !== 'GET') {
+ $request = $request->withMethod('GET');
+ $request = $request->withoutHeader('Transfer-Encoding');
+ $request = $request->withoutHeader('Content-Length');
+ $request = $request->withoutHeader('Content-Type');
+ $request = $request->withBody(null);
+ }
+ } else {
+ // We ALWAYS follow with a GET and without any set headers or body for redirects to other hosts.
+ $optionsWithoutHeaders = $options;
+ unset($optionsWithoutHeaders[self::OP_DEFAULT_HEADERS]);
+
+ $request = new Request((string)$redirectUri);
+ $request = $this->normalizeRequestHeaders($request, $redirectUri, $optionsWithoutHeaders);
+ }
+
+ if ($options[self::OP_AUTO_REFERER]) {
+ $request = $this->assignRedirectRefererHeader($request, $originalUri, $redirectUri);
+ }
+
+ $previousResponse = $response;
+ $originalUri = $redirectUri;
+ $uri = $redirectUri;
+ } else {
+ break;
+ }
+ } while (++$requestNr <= $maxRedirects + 1);
+
+ if ($maxRedirects !== 0 && $redirectUri = $this->getRedirectUri($response)) {
+ throw new TooManyRedirectsException($response);
+ }
+
+ return $response;
+ });
+ }
+
+ private function validateOption(string $option, $value)
+ {
+ switch ($option) {
+ case self::OP_AUTO_ENCODING:
+ if (!\is_bool($value)) {
+ throw new \TypeError("Invalid value for OP_AUTO_ENCODING, bool expected");
+ }
+
+ break;
+
+ case self::OP_TRANSFER_TIMEOUT:
+ if (!\is_int($value) || $value < 0) {
+ throw new \Error("Invalid value for OP_TRANSFER_TIMEOUT, int >= 0 expected");
+ }
+
+ break;
+
+ case self::OP_MAX_REDIRECTS:
+ if (!\is_int($value) || $value < 0) {
+ throw new \Error("Invalid value for OP_MAX_REDIRECTS, int >= 0 expected");
+ }
+
+ break;
+
+ case self::OP_AUTO_REFERER:
+ if (!\is_bool($value)) {
+ throw new \TypeError("Invalid value for OP_AUTO_REFERER, bool expected");
+ }
+
+ break;
+
+ case self::OP_DISCARD_BODY:
+ if (!\is_bool($value)) {
+ throw new \TypeError("Invalid value for OP_DISCARD_BODY, bool expected");
+ }
+
+ break;
+
+ case self::OP_DEFAULT_HEADERS:
+ // We attempt to set the headers here, because they're automatically validated then.
+ (new Request("https://example.com/"))->withHeaders($value);
+
+ break;
+
+ case self::OP_MAX_HEADER_BYTES:
+ if (!\is_int($value) || $value < 0) {
+ throw new \Error("Invalid value for OP_MAX_HEADER_BYTES, int >= 0 expected");
+ }
+
+ break;
+
+ case self::OP_MAX_BODY_BYTES:
+ if (!\is_int($value) || $value < 0) {
+ throw new \Error("Invalid value for OP_MAX_BODY_BYTES, int >= 0 expected");
+ }
+
+ break;
+
+ default:
+ throw new \Error(
+ sprintf("Unknown option: %s", $option)
+ );
+ }
+ }
+
+ private function generateRequestFromUri($uriOrRequest)
+ {
+ if (is_string($uriOrRequest)) {
+ $uri = $this->buildUriFromString($uriOrRequest);
+ $request = new Request($uri);
+ } elseif ($uriOrRequest instanceof Request) {
+ $uri = $this->buildUriFromString($uriOrRequest->getUri());
+ $request = $uriOrRequest;
+ } else {
+ throw new HttpException(
+ 'Request must be a valid HTTP URI or Amp\Artax\Request instance'
+ );
+ }
+
+ return [$request, $uri];
+ }
+
+ private function buildUriFromString($str): Uri
+ {
+ try {
+ $uri = new Uri($str);
+ $scheme = $uri->getScheme();
+
+ if (($scheme === "http" || $scheme === "https") && $uri->getHost()) {
+ return $uri;
+ }
+
+ throw new HttpException("Request must specify a valid HTTP URI");
+ } catch (InvalidUriException $e) {
+ throw new HttpException("Request must specify a valid HTTP URI", 0, $e);
+ }
+ }
+
+ private function normalizeRequestBodyHeaders(Request $request): \Generator
+ {
+ if ($request->hasHeader("Transfer-Encoding")) {
+ return $request->withoutHeader("Content-Length");
+ }
+
+ if ($request->hasHeader("Content-Length")) {
+ return $request;
+ }
+
+ /** @var RequestBody $body */
+ $body = $request->getBody();
+ $bodyLength = yield $body->getBodyLength();
+
+ if ($bodyLength === 0) {
+ $request = $request->withHeader('Content-Length', '0');
+ $request = $request->withoutHeader('Transfer-Encoding');
+ } else {
+ if ($bodyLength > 0) {
+ $request = $request->withHeader("Content-Length", $bodyLength);
+ $request = $request->withoutHeader("Transfer-Encoding");
+ } else {
+ $request = $request->withHeader("Transfer-Encoding", "chunked");
+ }
+ }
+
+ return $request;
+ }
+
+ private function normalizeRequestHeaders($request, $uri, $options)
+ {
+ $request = $this->normalizeRequestEncodingHeaderForZlib($request, $options);
+ $request = $this->normalizeRequestHostHeader($request, $uri);
+ $request = $this->normalizeRequestUserAgent($request);
+ $request = $this->normalizeRequestAcceptHeader($request);
+ $request = $this->assignApplicableRequestCookies($request);
+
+ return $request;
+ }
+
+ private function normalizeRequestEncodingHeaderForZlib(Request $request, array $options): Request
+ {
+ $autoEncoding = $options[self::OP_AUTO_ENCODING];
+
+ if (!$autoEncoding) {
+ return $request;
+ }
+
+ if ($this->hasZlib) {
+ return $request->withHeader('Accept-Encoding', 'gzip, deflate, identity');
+ }
+
+ return $request->withoutHeader('Accept-Encoding');
+ }
+
+ private function normalizeRequestHostHeader(Request $request, Uri $uri): Request
+ {
+ if ($request->hasHeader('Host')) {
+ return $request;
+ }
+
+ $authority = $this->generateAuthorityFromUri($uri);
+ $request = $request->withHeader('Host', $this->normalizeHostHeader($authority));
+
+ return $request;
+ }
+
+ private function generateAuthorityFromUri(Uri $uri): string
+ {
+ $host = $uri->getHost();
+ $port = $uri->getPort();
+
+ return "{$host}:{$port}";
+ }
+
+ private function normalizeHostHeader(string $host): string
+ {
+ // Though servers are supposed to be able to handle standard port names on the end of the
+ // Host header some fail to do this correctly. As a result, we strip the port from the end
+ // if it's a standard 80 or 443
+ if (strpos($host, ':80') === strlen($host) - 3) {
+ return substr($host, 0, -3);
+ } elseif (strpos($host, ':443') === strlen($host) - 4) {
+ return substr($host, 0, -4);
+ }
+
+ return $host;
+ }
+
+ private function normalizeRequestUserAgent(Request $request): Request
+ {
+ if ($request->hasHeader('User-Agent')) {
+ return $request;
+ }
+
+ return $request->withHeader('User-Agent', self::DEFAULT_USER_AGENT);
+ }
+
+ private function normalizeRequestAcceptHeader(Request $request): Request
+ {
+ if ($request->hasHeader('Accept')) {
+ return $request;
+ }
+
+ return $request->withHeader('Accept', '*/*');
+ }
+
+ private function assignApplicableRequestCookies(Request $request): Request
+ {
+ $uri = new Uri($request->getUri());
+
+ $domain = $uri->getHost();
+ $path = $uri->getPath();
+
+ if (!$applicableCookies = $this->cookieJar->get($domain, $path)) {
+ // No cookies matched our request; we're finished.
+ return $request->withoutHeader("Cookie");
+ }
+
+ $isRequestSecure = strcasecmp($uri->getScheme(), "https") === 0;
+ $cookiePairs = [];
+
+ /** @var Cookie $cookie */
+ foreach ($applicableCookies as $cookie) {
+ if (!$cookie->isSecure() || $isRequestSecure) {
+ $cookiePairs[] = $cookie->getName() . "=" . $cookie->getValue();
+ }
+ }
+
+ if ($cookiePairs) {
+ return $request->withHeader("Cookie", \implode("; ", $cookiePairs));
+ }
+
+ return $request->withoutHeader("Cookie");
+ }
+
+ private function normalizeTraceRequest(Request $request): Request
+ {
+ $method = $request->getMethod();
+
+ if ($method !== 'TRACE') {
+ return $request;
+ }
+
+ // https://tools.ietf.org/html/rfc7231#section-4.3.8
+ /** @var Request $request */
+ $request = $request->withBody(null);
+
+ // Remove all body and sensitive headers
+ $request = $request->withHeaders([
+ "Transfer-Encoding" => [],
+ "Content-Length" => [],
+ "Authorization" => [],
+ "Proxy-Authorization" => [],
+ "Cookie" => [],
+ ]);
+
+ return $request;
+ }
+
+ private function doRequest(Request $request, Uri $uri, array $options, Response $previousResponse = null, CancellationToken $cancellation): Promise
+ {
+ $deferred = new Deferred;
+
+ $requestCycle = new RequestCycle;
+ $requestCycle->request = $request;
+ $requestCycle->uri = $uri;
+ $requestCycle->options = $options;
+ $requestCycle->previousResponse = $previousResponse;
+ $requestCycle->deferred = $deferred;
+ $requestCycle->bodyDeferred = new Deferred;
+ $requestCycle->body = new Emitter;
+ $requestCycle->cancellation = $cancellation;
+
+ $protocolVersions = $request->getProtocolVersions();
+
+ if (\in_array("1.1", $protocolVersions, true)) {
+ $requestCycle->protocolVersion = "1.1";
+ } elseif (\in_array("1.0", $protocolVersions, true)) {
+ $requestCycle->protocolVersion = "1.0";
+ } else {
+ return new Failure(new HttpException(
+ "None of the requested protocol versions are supported: " . \implode(", ", $protocolVersions)
+ ));
+ }
+
+ asyncCall(function () use ($requestCycle) {
+ try {
+ yield from $this->doWrite($requestCycle);
+ } catch (\Throwable $e) {
+ $this->fail($requestCycle, $e);
+ }
+ });
+
+ return $deferred->promise();
+ }
+
+ private function doWrite(RequestCycle $requestCycle)
+ {
+ $timeout = $requestCycle->options[self::OP_TRANSFER_TIMEOUT];
+ $timeoutToken = new NullCancellationToken;
+
+ if ($timeout > 0) {
+ $transferTimeoutWatcher = Loop::delay($timeout, function () use ($requestCycle, $timeout) {
+ $this->fail($requestCycle, new TimeoutException(
+ sprintf('Allowed transfer timeout exceeded: %d ms', $timeout)
+ ));
+ });
+
+ $requestCycle->bodyDeferred->promise()->onResolve(static function () use ($transferTimeoutWatcher) {
+ Loop::cancel($transferTimeoutWatcher);
+ });
+
+ $timeoutToken = new TimeoutCancellationToken($timeout);
+ }
+
+ $authority = $this->generateAuthorityFromUri($requestCycle->uri);
+ $socketCheckoutUri = $requestCycle->uri->getScheme() . "://{$authority}";
+ $connectTimeoutToken = new CombinedCancellationToken($requestCycle->cancellation, $timeoutToken);
+
+ try {
+ /** @var ClientSocket $socket */
+ $socket = yield $this->socketPool->checkout($socketCheckoutUri, $connectTimeoutToken);
+ $requestCycle->socket = $socket;
+ } catch (ResolutionException $dnsException) {
+ throw new DnsException(\sprintf("Resolving the specified domain failed: '%s'", $requestCycle->uri->getHost()), 0, $dnsException);
+ } catch (ConnectException $e) {
+ throw new SocketException(\sprintf("Connection to '%s' failed", $authority), 0, $e);
+ } catch (CancelledException $e) {
+ // In case of a user cancellation request, throw the expected exception
+ $requestCycle->cancellation->throwIfRequested();
+
+ // Otherwise we ran into a timeout of our TimeoutCancellationToken
+ throw new SocketException(\sprintf("Connection to '%s' timed out", $authority), 0, $e);
+ }
+
+ $cancellation = $requestCycle->cancellation->subscribe(function ($error) use ($requestCycle) {
+ $this->fail($requestCycle, $error);
+ });
+
+ try {
+ if ($requestCycle->uri->getScheme() === 'https') {
+ $tlsContext = $this->tlsContext
+ ->withPeerName($requestCycle->uri->getHost())
+ ->withPeerCapturing();
+
+ yield $socket->enableCrypto($tlsContext);
+ }
+
+ // Collect this here, because it fails in case the remote closes the connection directly.
+ $connectionInfo = $this->collectConnectionInfo($socket);
+
+ $rawHeaders = $this->generateRawRequestHeaders($requestCycle->request, $requestCycle->protocolVersion);
+ yield $socket->write($rawHeaders);
+
+ $body = $requestCycle->request->getBody()->createBodyStream();
+ $chunking = $requestCycle->request->getHeader("transfer-encoding") === "chunked";
+ $remainingBytes = $requestCycle->request->getHeader("content-length");
+
+ if ($chunking && $requestCycle->protocolVersion === "1.0") {
+ throw new HttpException("Can't send chunked bodies over HTTP/1.0");
+ }
+
+ // We always buffer the last chunk to make sure we don't write $contentLength bytes if the body is too long.
+ $buffer = "";
+
+ while (null !== $chunk = yield $body->read()) {
+ $requestCycle->cancellation->throwIfRequested();
+
+ if ($chunk === "") {
+ continue;
+ }
+
+ if ($chunking) {
+ $chunk = \dechex(\strlen($chunk)) . "\r\n" . $chunk . "\r\n";
+ }/* elseif ($remainingBytes !== null) {
+ $remainingBytes -= \strlen($chunk);
+
+ if ($remainingBytes < 0) {
+ throw new HttpException("Body contained more bytes than specified in Content-Length, aborting request");
+ }
+ }*/
+
+ yield $socket->write($buffer);
+ $buffer = $chunk;
+ }
+
+ // Flush last buffered chunk.
+ yield $socket->write($buffer);
+
+ if ($chunking) {
+ yield $socket->write("0\r\n\r\n");
+ }/* elseif ($remainingBytes !== null && $remainingBytes > 0) {
+ throw new HttpException("Body contained fewer bytes than specified in Content-Length, aborting request");
+ }*/
+
+ yield from $this->doRead($requestCycle, $socket, $connectionInfo);
+ } finally {
+ $requestCycle->cancellation->unsubscribe($cancellation);
+ }
+ }
+
+ private function fail(RequestCycle $requestCycle, \Throwable $error)
+ {
+ $toFails = [];
+ $socket = null;
+
+ if ($requestCycle->deferred) {
+ $toFails[] = $requestCycle->deferred;
+ $requestCycle->deferred = null;
+ }
+
+ if ($requestCycle->body) {
+ $toFails[] = $requestCycle->body;
+ $requestCycle->body = null;
+ }
+
+ if ($requestCycle->bodyDeferred) {
+ $toFails[] = $requestCycle->bodyDeferred;
+ $requestCycle->bodyDeferred = null;
+ }
+
+ if ($requestCycle->socket) {
+ $this->socketPool->clear($requestCycle->socket);
+ $socket = $requestCycle->socket;
+ $requestCycle->socket = null;
+ $socket->close();
+ }
+
+ foreach ($toFails as $toFail) {
+ $toFail->fail($error);
+ }
+ }
+
+ private function collectConnectionInfo(ClientSocket $socket): ConnectionInfo
+ {
+ $crypto = \stream_get_meta_data($socket->getResource())["crypto"] ?? null;
+
+ return new ConnectionInfo(
+ $socket->getLocalAddress(),
+ $socket->getRemoteAddress(),
+ $crypto ? TlsInfo::fromMetaData($crypto, \stream_context_get_options($socket->getResource())["ssl"]) : null
+ );
+ }
+
+ /**
+ * @param Request $request
+ * @param string $protocolVersion
+ *
+ * @return string
+ *
+ * @TODO Send absolute URIs in the request line when using a proxy server
+ * Right now this doesn't matter because all proxy requests use a CONNECT
+ * tunnel but this likely will not always be the case.
+ */
+ private function generateRawRequestHeaders(Request $request, string $protocolVersion): string
+ {
+ $uri = $request->getUri();
+ $uri = new Uri($uri);
+
+ $requestUri = $uri->getPath() ?: '/';
+
+ if ($query = $uri->getQuery()) {
+ $requestUri .= '?' . $query;
+ }
+
+ $head = $request->getMethod() . ' ' . $requestUri . ' HTTP/' . $protocolVersion . "\r\n";
+
+ $headers = $request->getHeaders(true);
+ /*$newHeaders = [];
+
+ foreach($headers as $key => $val)
+ {
+ if ($key !== 'Content-Length')
+ {
+ $newHeaders[$key] = $val;
+ }
+ }*/
+
+ // Curse you Kitsu, for this stupid work-around because the login API endpoint doesn't allow for a Content-Length header!
+ //unset($headers['Content-Length']);
+
+ foreach ($headers as $field => $values) {
+ if (\strcspn($field, "\r\n") !== \strlen($field)) {
+ throw new HttpException("Blocked header injection attempt for header '{$field}'");
+ }
+
+ foreach ($values as $value) {
+ if (\strcspn($value, "\r\n") !== \strlen($value)) {
+ throw new HttpException("Blocked header injection attempt for header '{$field}' with value '{$value}'");
+ }
+
+ $head .= "{$field}: {$value}\r\n";
+ }
+ }
+
+ $head .= "\r\n";
+
+ return $head;
+ }
+
+ private function doRead(RequestCycle $requestCycle, ClientSocket $socket, ConnectionInfo $connectionInfo): \Generator
+ {
+ try {
+ $backpressure = new Success;
+ $bodyCallback = $requestCycle->options[self::OP_DISCARD_BODY]
+ ? null
+ : static function ($data) use ($requestCycle, &$backpressure) {
+ $backpressure = $requestCycle->body->emit($data);
+ };
+
+ $parser = new Parser($bodyCallback);
+
+ $parser->enqueueResponseMethodMatch($requestCycle->request->getMethod());
+ $parser->setAllOptions([
+ Parser::OP_MAX_HEADER_BYTES => $requestCycle->options[self::OP_MAX_HEADER_BYTES],
+ Parser::OP_MAX_BODY_BYTES => $requestCycle->options[self::OP_MAX_BODY_BYTES],
+ ]);
+
+ while (null !== $chunk = yield $socket->read()) {
+ $requestCycle->cancellation->throwIfRequested();
+
+ $parseResult = $parser->parse($chunk);
+
+ if (!$parseResult) {
+ continue;
+ }
+
+ $parseResult["headers"] = \array_change_key_case($parseResult["headers"], \CASE_LOWER);
+
+ $response = $this->finalizeResponse($requestCycle, $parseResult, $connectionInfo);
+ $shouldCloseSocketAfterResponse = $this->shouldCloseSocketAfterResponse($response);
+ $ignoreIncompleteBodyCheck = false;
+ $responseHeaders = $response->getHeaders();
+
+ if ($requestCycle->deferred) {
+ $deferred = $requestCycle->deferred;
+ $requestCycle->deferred = null;
+ $deferred->resolve($response);
+ $response = null; // clear references
+ $deferred = null; // there's also a reference in the deferred
+ } else {
+ return;
+ }
+
+ // Required, otherwise responses without body hang
+ if ($parseResult["headersOnly"]) {
+ // Directly parse again in case we already have the full body but aborted parsing
+ // to resolve promise with headers.
+ $chunk = null;
+
+ do {
+ try {
+ $parseResult = $parser->parse($chunk);
+ } catch (ParseException $e) {
+ $this->fail($requestCycle, $e);
+ throw $e;
+ }
+
+ if ($parseResult) {
+ break;
+ }
+
+ if (!$backpressure instanceof Success) {
+ yield $this->withCancellation($backpressure, $requestCycle->cancellation);
+ }
+
+ if ($requestCycle->bodyTooLarge) {
+ throw new HttpException("Response body exceeded the specified size limit");
+ }
+ } while (null !== $chunk = yield $socket->read());
+
+ $parserState = $parser->getState();
+ if ($parserState !== Parser::AWAITING_HEADERS) {
+ // Ignore check if neither content-length nor chunked encoding are given.
+ $ignoreIncompleteBodyCheck = $parserState === Parser::BODY_IDENTITY_EOF &&
+ !isset($responseHeaders["content-length"]) &&
+ strcasecmp('identity', $responseHeaders['transfer-encoding'][0] ?? "");
+
+ if (!$ignoreIncompleteBodyCheck) {
+ throw new SocketException(sprintf(
+ 'Socket disconnected prior to response completion (Parser state: %s)',
+ $parserState
+ ));
+ }
+ }
+ }
+
+ if ($shouldCloseSocketAfterResponse || $ignoreIncompleteBodyCheck) {
+ $this->socketPool->clear($socket);
+ $socket->close();
+ } else {
+ $this->socketPool->checkin($socket);
+ }
+
+ $requestCycle->socket = null;
+
+ // Complete body AFTER socket checkin, so the socket can be reused for a potential redirect
+ $body = $requestCycle->body;
+ $requestCycle->body = null;
+
+ $bodyDeferred = $requestCycle->bodyDeferred;
+ $requestCycle->bodyDeferred = null;
+
+ $body->complete();
+ $bodyDeferred->resolve();
+
+ return;
+ }
+ } catch (\Throwable $e) {
+ $this->fail($requestCycle, $e);
+
+ return;
+ }
+
+ if ($socket->getResource() !== null) {
+ $requestCycle->socket = null;
+ $this->socketPool->clear($socket);
+ $socket->close();
+ }
+
+ // Required, because if the write fails, the read() call immediately resolves.
+ yield new Delayed(0);
+
+ if ($requestCycle->deferred === null) {
+ return;
+ }
+
+ $parserState = $parser->getState();
+
+ if ($parserState === Parser::AWAITING_HEADERS && $requestCycle->retryCount < 1) {
+ $requestCycle->retryCount++;
+ yield from $this->doWrite($requestCycle);
+ } else {
+ $this->fail($requestCycle, new SocketException(sprintf(
+ 'Socket disconnected prior to response completion (Parser state: %s)',
+ $parserState
+ )));
+ }
+ }
+
+ private function finalizeResponse(RequestCycle $requestCycle, array $parserResult, ConnectionInfo $connectionInfo)
+ {
+ $body = new IteratorStream($requestCycle->body->iterate());
+
+ if ($encoding = $this->determineCompressionEncoding($parserResult["headers"])) {
+ $body = new ZlibInputStream($body, $encoding);
+ }
+
+ // Wrap the input stream so we can discard the body in case it's destructed but hasn't been consumed.
+ // This allows reusing the connection for further requests. It's important to have __destruct in InputStream and
+ // not in Message, because an InputStream might be pulled out of Message and used separately.
+ $body = new class($body, $requestCycle, $this->socketPool) implements InputStream
+ {
+ private $body;
+ private $bodySize = 0;
+ private $requestCycle;
+ private $socketPool;
+ private $successfulEnd = false;
+
+ public function __construct(InputStream $body, RequestCycle $requestCycle, HttpSocketPool $socketPool)
+ {
+ $this->body = $body;
+ $this->requestCycle = $requestCycle;
+ $this->socketPool = $socketPool;
+ }
+
+ public function read(): Promise
+ {
+ $promise = $this->body->read();
+ $promise->onResolve(function ($error, $value) {
+ if ($value !== null) {
+ $this->bodySize += \strlen($value);
+ $maxBytes = $this->requestCycle->options[Client::OP_MAX_BODY_BYTES];
+ if ($maxBytes !== 0 && $this->bodySize >= $maxBytes) {
+ $this->requestCycle->bodyTooLarge = true;
+ }
+ } elseif ($error === null) {
+ $this->successfulEnd = true;
+ }
+ });
+
+ return $promise;
+ }
+
+ public function __destruct()
+ {
+ if (!$this->successfulEnd && $this->requestCycle->socket) {
+ $this->socketPool->clear($this->requestCycle->socket);
+ $socket = $this->requestCycle->socket;
+ $this->requestCycle->socket = null;
+ $socket->close();
+ }
+ }
+ };
+
+ $response = new class($parserResult["protocol"], $parserResult["status"], $parserResult["reason"], $parserResult["headers"], $body, $requestCycle->request, $requestCycle->previousResponse, new MetaInfo($connectionInfo)) implements Response
+ {
+ private $protocolVersion;
+ private $status;
+ private $reason;
+ private $request;
+ private $previousResponse;
+ private $headers;
+ private $body;
+ private $metaInfo;
+
+ public function __construct(
+ string $protocolVersion,
+ int $status,
+ string $reason,
+ array $headers,
+ InputStream $body,
+ Request $request,
+ Response $previousResponse = null,
+ MetaInfo $metaInfo
+ )
+ {
+ $this->protocolVersion = $protocolVersion;
+ $this->status = $status;
+ $this->reason = $reason;
+ $this->headers = $headers;
+ $this->body = new Message($body);
+ $this->request = $request;
+ $this->previousResponse = $previousResponse;
+ $this->metaInfo = $metaInfo;
+ }
+
+ public function getProtocolVersion(): string
+ {
+ return $this->protocolVersion;
+ }
+
+ public function getStatus(): int
+ {
+ return $this->status;
+ }
+
+ public function getReason(): string
+ {
+ return $this->reason;
+ }
+
+ public function getRequest(): Request
+ {
+ return $this->request;
+ }
+
+ public function getOriginalRequest(): Request
+ {
+ if (empty($this->previousResponse)) {
+ return $this->request;
+ }
+
+ return $this->previousResponse->getOriginalRequest();
+ }
+
+ public function getPreviousResponse()
+ {
+ return $this->previousResponse;
+ }
+
+ public function hasHeader(string $field): bool
+ {
+ return isset($this->headers[\strtolower($field)]);
+ }
+
+ public function getHeader(string $field)
+ {
+ return $this->headers[\strtolower($field)][0] ?? null;
+ }
+
+ public function getHeaderArray(string $field): array
+ {
+ return $this->headers[\strtolower($field)] ?? [];
+ }
+
+ public function getHeaders(): array
+ {
+ return $this->headers;
+ }
+
+ public function getBody(): Message
+ {
+ return $this->body;
+ }
+
+ public function getMetaInfo(): MetaInfo
+ {
+ return $this->metaInfo;
+ }
+ };
+
+ if ($response->hasHeader('Set-Cookie')) {
+ $requestDomain = $requestCycle->uri->getHost();
+ $cookies = $response->getHeaderArray('Set-Cookie');
+
+ foreach ($cookies as $rawCookieStr) {
+ $this->storeResponseCookie($requestDomain, $rawCookieStr);
+ }
+ }
+
+ return $response;
+ }
+
+ private function determineCompressionEncoding(array $responseHeaders): int
+ {
+ if (!$this->hasZlib) {
+ return 0;
+ }
+
+ if (!isset($responseHeaders["content-encoding"])) {
+ return 0;
+ }
+
+ $contentEncodingHeader = \trim(\current($responseHeaders["content-encoding"]));
+
+ if (strcasecmp($contentEncodingHeader, 'gzip') === 0) {
+ return \ZLIB_ENCODING_GZIP;
+ }
+
+ if (strcasecmp($contentEncodingHeader, 'deflate') === 0) {
+ return \ZLIB_ENCODING_DEFLATE;
+ }
+
+ return 0;
+ }
+
+ private function storeResponseCookie(string $requestDomain, string $rawCookieStr)
+ {
+ try {
+ $cookie = Cookie::fromString($rawCookieStr);
+
+ if (!$cookie->getDomain()) {
+ $cookie = $cookie->withDomain($requestDomain);
+ } else {
+ // https://tools.ietf.org/html/rfc6265#section-4.1.2.3
+ $cookieDomain = $cookie->getDomain();
+
+ // If a domain is set, left dots are ignored and it's always a wildcard
+ $cookieDomain = \ltrim($cookieDomain, ".");
+
+ if ($cookieDomain !== $requestDomain) {
+ // ignore cookies on domains that are public suffixes
+ if (PublicSuffixList::isPublicSuffix($cookieDomain)) {
+ return;
+ }
+
+ // cookie origin would not be included when sending the cookie
+ if (\substr($requestDomain, 0, -\strlen($cookieDomain) - 1) . "." . $cookieDomain !== $requestDomain) {
+ return;
+ }
+ }
+
+ // always add the dot, it's used internally for wildcard matching when an explicit domain is sent
+ $cookie = $cookie->withDomain("." . $cookieDomain);
+ }
+
+ $this->cookieJar->store($cookie);
+ } catch (CookieFormatException $e) {
+ // Ignore malformed Set-Cookie headers
+ }
+ }
+
+ private function shouldCloseSocketAfterResponse(Response $response)
+ {
+ $request = $response->getRequest();
+
+ $requestConnHeader = $request->getHeader('Connection');
+ $responseConnHeader = $response->getHeader('Connection');
+
+ if ($requestConnHeader && !strcasecmp($requestConnHeader, 'close')) {
+ return true;
+ } elseif ($responseConnHeader && !strcasecmp($responseConnHeader, 'close')) {
+ return true;
+ } elseif ($response->getProtocolVersion() === '1.0' && !$responseConnHeader) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private function withCancellation(Promise $promise, CancellationToken $cancellationToken): Promise
+ {
+ $deferred = new Deferred;
+ $newPromise = $deferred->promise();
+
+ $promise->onResolve(function ($error, $value) use (&$deferred) {
+ if ($deferred) {
+ if ($error) {
+ $deferred->fail($error);
+ $deferred = null;
+ } else {
+ $deferred->resolve($value);
+ $deferred = null;
+ }
+ }
+ });
+
+ $cancellationSubscription = $cancellationToken->subscribe(function ($e) use (&$deferred) {
+ if ($deferred) {
+ $deferred->fail($e);
+ $deferred = null;
+ }
+ });
+
+ $newPromise->onResolve(function () use ($cancellationToken, $cancellationSubscription) {
+ $cancellationToken->unsubscribe($cancellationSubscription);
+ });
+
+ return $newPromise;
+ }
+
+ private function getRedirectUri(Response $response)
+ {
+ if (!$response->hasHeader('Location')) {
+ return null;
+ }
+
+ $request = $response->getRequest();
+
+ $status = $response->getStatus();
+ $method = $request->getMethod();
+
+ if ($status < 300 || $status > 399 || $method === 'HEAD') {
+ return null;
+ }
+
+ $requestUri = new Uri($request->getUri());
+ $redirectLocation = $response->getHeader('Location');
+
+ try {
+ return $requestUri->resolve($redirectLocation);
+ } catch (InvalidUriException $e) {
+ return null;
+ }
+ }
+
+ /**
+ * Clients must not add a Referer header when leaving an unencrypted resource and redirecting to an encrypted
+ * resource.
+ *
+ * @param Request $request
+ * @param string $refererUri
+ * @param string $newUri
+ *
+ * @return Request
+ *
+ * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec15.html#sec15.1.3
+ */
+ private function assignRedirectRefererHeader(Request $request, string $refererUri, string $newUri): Request
+ {
+ $refererIsEncrypted = (\stripos($refererUri, 'https') === 0);
+ $destinationIsEncrypted = (\stripos($newUri, 'https') === 0);
+
+ if (!$refererIsEncrypted || $destinationIsEncrypted) {
+ return $request->withHeader('Referer', $refererUri);
+ }
+
+ return $request->withoutHeader('Referer');
+ }
+
+ /**
+ * Set multiple options at once.
+ *
+ * @param array $options An array of the form [OP_CONSTANT => $value]
+ *
+ * @throws \Error On unknown option key or invalid value.
+ */
+ public function setOptions(array $options)
+ {
+ foreach ($options as $option => $value) {
+ $this->setOption($option, $value);
+ }
+ }
+
+ /**
+ * Set an option.
+ *
+ * @param string $option A Client option constant
+ * @param mixed $value The option value to assign
+ *
+ * @throws \Error On unknown option key or invalid value.
+ */
+ public function setOption(string $option, $value)
+ {
+ $this->validateOption($option, $value);
+ $this->options[$option] = $value;
+ }
+}
diff --git a/src/API/Kitsu/Auth.php b/src/API/Kitsu/Auth.php
index 743c1d95..7ff53ad3 100644
--- a/src/API/Kitsu/Auth.php
+++ b/src/API/Kitsu/Auth.php
@@ -73,14 +73,14 @@ class Auth {
$config = $this->container->get('config');
$username = $config->get(['kitsu_username']);
- try
+ // try
{
$auth = $this->model->authenticate($username, $password);
}
- catch (Exception $e)
+ /* catch (Exception $e)
{
return FALSE;
- }
+ }*/
if (FALSE !== $auth)
diff --git a/src/API/Kitsu/KitsuRequestBuilder.php b/src/API/Kitsu/KitsuRequestBuilder.php
index a7a0f585..46e7bf97 100644
--- a/src/API/Kitsu/KitsuRequestBuilder.php
+++ b/src/API/Kitsu/KitsuRequestBuilder.php
@@ -16,10 +16,7 @@
namespace Aviat\AnimeClient\API\Kitsu;
-use Aviat\AnimeClient\API\{
- APIRequestBuilder,
- Kitsu as K
-};
+use Aviat\AnimeClient\API\APIRequestBuilder;
class KitsuRequestBuilder extends APIRequestBuilder {
@@ -27,7 +24,7 @@ class KitsuRequestBuilder extends APIRequestBuilder {
* The base url for api requests
* @var string $base_url
*/
- protected $baseUrl = "https://kitsu.io/api/edge/";
+ protected $baseUrl = 'https://kitsu.io/api/edge/';
/**
* HTTP headers to send with every request
@@ -36,9 +33,9 @@ class KitsuRequestBuilder extends APIRequestBuilder {
*/
protected $defaultHeaders = [
'User-Agent' => "Tim's Anime Client/4.0",
- 'Accept-Encoding' => 'application/vnd.api+json',
+ 'Accept' => 'application/vnd.api+json',
'Content-Type' => 'application/vnd.api+json',
- 'client_id' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
- 'client_secret' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151',
+ 'CLIENT_ID' => 'dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd',
+ 'CLIENT_SECRET' => '54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151',
];
}
\ No newline at end of file
diff --git a/src/API/Kitsu/KitsuTrait.php b/src/API/Kitsu/KitsuTrait.php
index fe2167b7..215c9bcc 100644
--- a/src/API/Kitsu/KitsuTrait.php
+++ b/src/API/Kitsu/KitsuTrait.php
@@ -18,12 +18,17 @@ namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\SESSION_SEGMENT;
-use function Amp\wait;
+use function Amp\Promise\wait;
-use Amp\Artax\{Client, Request};
+use Amp\Artax\Request;
use Aviat\AnimeClient\AnimeClient;
-use Aviat\AnimeClient\API\{FailedResponseException, Kitsu as K};
+use Aviat\AnimeClient\API\{
+ FailedResponseException,
+ HummingbirdClient,
+ Kitsu as K
+};
use Aviat\Ion\Json;
+use Aviat\Ion\JsonException;
trait KitsuTrait {
@@ -80,24 +85,29 @@ trait KitsuTrait {
$token = $cacheItem->get();
}
- if ( ! is_null($token))
+ if (NULL !== $token)
{
$request = $request->setAuth('bearer', $token);
}
if (array_key_exists('form_params', $options))
{
- $request->setFormFields($options['form_params']);
+ $request = $request->setFormFields($options['form_params']);
}
if (array_key_exists('query', $options))
{
- $request->setQuery($options['query']);
+ $request = $request->setQuery($options['query']);
}
if (array_key_exists('body', $options))
{
- $request->setJsonBody($options['body']);
+ $request = $request->setJsonBody($options['body']);
+ }
+
+ if (array_key_exists('headers', $options))
+ {
+ $request = $request->setHeaders($options['headers']);
}
return $request->getFullRequest();
@@ -113,9 +123,24 @@ trait KitsuTrait {
*/
private function getResponse(string $type, string $url, array $options = [])
{
+ $logger = NULL;
+ if ($this->getContainer())
+ {
+ $logger = $this->container->getLogger('kitsu-request');
+ }
+
$request = $this->setUpRequest($type, $url, $options);
- $response = wait((new Client)->request($request));
+ $response = wait((new HummingbirdClient)->request($request));
+
+ if ($logger)
+ {
+ $logger->debug('Kitsu API Response', [
+ 'response_status' => $response->getStatus(),
+ 'request_headers' => $response->getOriginalRequest()->getHeaders(),
+ 'response_headers' => $response->getHeaders()
+ ]);
+ }
return $response;
}
@@ -126,6 +151,8 @@ trait KitsuTrait {
* @param string $type
* @param string $url
* @param array $options
+ * @throws \Aviat\Ion\JsonException
+ * @throws FailedResponseException
* @return array
*/
private function request(string $type, string $url, array $options = []): array
@@ -148,7 +175,16 @@ trait KitsuTrait {
throw new FailedResponseException('Failed to get the proper response from the API');
}
- return Json::decode($response->getBody(), TRUE);
+ try
+ {
+ return Json::decode(wait($response->getBody()), TRUE);
+ }
+ catch (JsonException $e)
+ {
+ print_r($e);
+ die();
+ }
+
}
/**
@@ -198,7 +234,7 @@ trait KitsuTrait {
}
}
- return JSON::decode($response->getBody(), TRUE);
+ return JSON::decode(wait($response->getBody()), TRUE);
}
/**
diff --git a/src/API/Kitsu/ListItem.php b/src/API/Kitsu/ListItem.php
index bd852daa..71bef41a 100644
--- a/src/API/Kitsu/ListItem.php
+++ b/src/API/Kitsu/ListItem.php
@@ -18,8 +18,10 @@ namespace Aviat\AnimeClient\API\Kitsu;
use const Aviat\AnimeClient\SESSION_SEGMENT;
+use function Amp\Promise\wait;
+
use Amp\Artax\Request;
-use Aviat\AnimeClient\API\AbstractListItem;
+use Aviat\AnimeClient\API\{AbstractListItem, HummingbirdClient};
use Aviat\Ion\Di\ContainerAware;
use Aviat\Ion\Json;
use RuntimeException;
@@ -113,21 +115,21 @@ class ListItem extends AbstractListItem {
public function get(string $id): array
{
$authHeader = $this->getAuthHeader();
-
+
$request = $this->requestBuilder->newRequest('GET', "library-entries/{$id}")
->setQuery([
'include' => 'media,media.genres,media.mappings'
]);
-
+
if ($authHeader !== FALSE)
{
$request = $request->setHeader('Authorization', $authHeader);
}
-
+
$request = $request->getFullRequest();
- $response = \Amp\wait((new \Amp\Artax\Client)->request($request));
- return Json::decode($response->getBody());
+ $response = wait((new HummingbirdClient)->request($request));
+ return Json::decode(wait($response->getBody()));
}
public function update(string $id, array $data): Request
@@ -140,15 +142,15 @@ class ListItem extends AbstractListItem {
'attributes' => $data
]
];
-
+
$request = $this->requestBuilder->newRequest('PATCH', "library-entries/{$id}")
->setJsonBody($requestData);
-
+
if ($authHeader !== FALSE)
{
$request = $request->setHeader('Authorization', $authHeader);
}
-
+
return $request->getFullRequest();
}
}
\ No newline at end of file
diff --git a/src/API/Kitsu/Model.php b/src/API/Kitsu/Model.php
index 74d9917b..f25e9be2 100644
--- a/src/API/Kitsu/Model.php
+++ b/src/API/Kitsu/Model.php
@@ -16,9 +16,9 @@
namespace Aviat\AnimeClient\API\Kitsu;
-use function Amp\{all, wait};
+use function Amp\Promise\wait;
-use Amp\Artax\{Client, Request};
+use Amp\Artax\Request;
use Aviat\AnimeClient\API\{
CacheTrait,
JsonAPI,
@@ -101,22 +101,37 @@ class Model {
*/
public function authenticate(string $username, string $password)
{
+ // K::AUTH_URL
$response = $this->getResponse('POST', K::AUTH_URL, [
- 'headers' => [],
+ 'headers' => [
+ 'accept' => NULL,
+ 'Content-type' => 'application/x-www-form-urlencoded',
+ 'client_id' => NULL,
+ 'client_secret' => NULL
+ ],
'form_params' => [
'grant_type' => 'password',
'username' => $username,
'password' => $password
]
]);
-
- $data = Json::decode((string)$response->getBody());
+ $data = Json::decode(wait($response->getBody()));
+
+ //dump($response);
if (array_key_exists('access_token', $data))
{
return $data;
}
+ if (array_key_exists('error', $data))
+ {
+ dump($data['error']);
+ dump($response);
+ die();
+ //throw new \Exception('auth error');
+ }
+
return FALSE;
}
@@ -129,14 +144,17 @@ class Model {
public function reAuthenticate(string $token)
{
$response = $this->getResponse('POST', K::AUTH_URL, [
- 'headers' => [],
+ 'headers' => [
+ 'Accept-encoding' => '*'
+
+ ],
'form_params' => [
'grant_type' => 'refresh_token',
'refresh_token' => $token
]
]);
- $data = Json::decode((string)$response->getBody());
+ $data = Json::decode(wait($response->getBody()));
if (array_key_exists('access_token', $data))
{
@@ -186,7 +204,7 @@ class Model {
*/
public function getCharacter(string $slug): array
{
- $data = $this->getRequest('/characters', [
+ $data = $this->getRequest('characters', [
'query' => [
'filter' => [
'slug' => $slug,
@@ -211,7 +229,7 @@ class Model {
public function getUserData(string $username): array
{
// $userId = $this->getUserIdByUsername($username);
- $data = $this->getRequest("/users", [
+ $data = $this->getRequest("users", [
'query' => [
'filter' => [
'name' => $username,
@@ -425,7 +443,7 @@ class Model {
foreach($responses as $response)
{
- $data = Json::decode($response->getBody());
+ $data = Json::decode($response);
$output = array_merge_recursive($output, $data);
}
@@ -522,7 +540,6 @@ class Model {
*/
public function getRawAnimeList(string $status): array
{
-
$options = [
'filter' => [
'user_id' => $this->getUserIdByUsername($this->getUsername()),
@@ -689,7 +706,7 @@ class Model {
foreach($responses as $response)
{
- $data = Json::decode($response->getBody());
+ $data = Json::decode($response);
$output = array_merge_recursive($output, $data);
}
diff --git a/src/API/MAL/ListItem.php b/src/API/MAL/ListItem.php
index e46199cf..27de85b3 100644
--- a/src/API/MAL/ListItem.php
+++ b/src/API/MAL/ListItem.php
@@ -94,9 +94,9 @@ class ListItem {
$config = $this->container->get('config');
$xml = XML::toXML(['entry' => $data]);
- $body = (new FormBody)
- ->addField('id', $id)
- ->addField('data', $xml);
+ $body = new FormBody();
+ $body->addField('id', $id);
+ $body->addField('data', $xml);
return $this->requestBuilder->newRequest('POST', "{$type}list/update/{$id}.xml")
->setFormFields([
diff --git a/src/API/MAL/MALTrait.php b/src/API/MAL/MALTrait.php
index 32bd299c..b82ffa2c 100644
--- a/src/API/MAL/MALTrait.php
+++ b/src/API/MAL/MALTrait.php
@@ -16,15 +16,13 @@
namespace Aviat\AnimeClient\API\MAL;
-use Amp\Artax\{Client, FormBody, Request};
+use function Amp\Promise\wait;
+
use Aviat\AnimeClient\API\{
+ HummingbirdClient,
MAL as M,
- APIRequestBuilder,
XML
};
-use Aviat\AnimeClient\API\MALRequestBuilder;
-use Aviat\Ion\Json;
-use InvalidArgumentException;
trait MALTrait {
@@ -82,12 +80,12 @@ trait MALTrait {
if (array_key_exists('query', $options))
{
- $request->setQuery($options['query']);
+ $request = $request->setQuery($options['query']);
}
if (array_key_exists('body', $options))
{
- $request->setBody($options['body']);
+ $request = $request->setBody($options['body']);
}
return $request->getFullRequest();
@@ -110,14 +108,14 @@ trait MALTrait {
}
$request = $this->setUpRequest($type, $url, $options);
- $response = \Amp\wait((new Client)->request($request));
+ $response = wait((new HummingbirdClient)->request($request));
$logger->debug('MAL api response', [
'status' => $response->getStatus(),
'reason' => $response->getReason(),
'body' => $response->getBody(),
- 'headers' => $response->getAllHeaders(),
- 'requestHeaders' => $request->getAllHeaders(),
+ 'headers' => $response->getHeaders(),
+ 'requestHeaders' => $request->getHeaders(),
]);
return $response;
@@ -149,7 +147,7 @@ trait MALTrait {
}
}
- return XML::toArray((string) $response->getBody());
+ return XML::toArray(wait($response->getBody()));
}
/**
diff --git a/src/API/ParallelAPIRequest.php b/src/API/ParallelAPIRequest.php
index a05ebec0..b7c1dc95 100644
--- a/src/API/ParallelAPIRequest.php
+++ b/src/API/ParallelAPIRequest.php
@@ -16,21 +16,21 @@
namespace Aviat\AnimeClient\API;
-use Amp;
-use Amp\Artax\Client;
+use function Amp\call;
+use function Amp\Promise\{all, wait};
/**
* Class to simplify making and validating simultaneous requests
*/
class ParallelAPIRequest {
-
+
/**
* Set of requests to make in parallel
*
* @var array
*/
protected $requests = [];
-
+
/**
* Add a request
*
@@ -45,11 +45,11 @@ class ParallelAPIRequest {
$this->requests[$key] = $request;
return $this;
}
-
+
$this->requests[] = $request;
return $this;
}
-
+
/**
* Add multiple requests
*
@@ -61,22 +61,27 @@ class ParallelAPIRequest {
array_walk($requests, [$this, 'addRequest']);
return $this;
}
-
+
/**
* Actually make the requests
*
- * @param bool $allowFailingRequests
- * @return array
+ * @return array
*/
- public function makeRequests(bool $allowFailingRequests = FALSE): array
+ public function makeRequests(): array
{
- $client = new Client();
- $promises = $client->requestMulti($this->requests);
-
- $func = ($allowFailingRequests) ? '\Amp\some' : '\Amp\all';
-
- $results = Amp\wait($func($promises));
-
- return $results;
+ $client = new HummingbirdClient();
+ $promises = [];
+
+ foreach ($this->requests as $key => $url)
+ {
+ $promises[$key] = call(function () use ($client, $url) {
+ $response = yield $client->request($url);
+ $body = yield $response->getBody();
+
+ return $body;
+ });
+ }
+
+ return wait(all($promises));
}
}
\ No newline at end of file
diff --git a/src/Controller/AnimeCollection.php b/src/Controller/AnimeCollection.php
index 2d3c1624..0bb096a8 100644
--- a/src/Controller/AnimeCollection.php
+++ b/src/Controller/AnimeCollection.php
@@ -21,7 +21,6 @@ use Aviat\AnimeClient\Model\{
Anime as AnimeModel,
AnimeCollection as AnimeCollectionModel
};
-use Aviat\AnimeClient\UrlGenerator;
use Aviat\Ion\Di\ContainerInterface;
/**
diff --git a/src/Controller/Index.php b/src/Controller/Index.php
index 6eb48148..439522c5 100644
--- a/src/Controller/Index.php
+++ b/src/Controller/Index.php
@@ -16,11 +16,10 @@
namespace Aviat\AnimeClient\Controller;
-use function Amp\wait;
+use function Amp\Promise\wait;
-use Amp\Artax\Client;
use Aviat\AnimeClient\Controller as BaseController;
-use Aviat\AnimeClient\API\JsonAPI;
+use Aviat\AnimeClient\API\{HummingbirdClient, JsonAPI};
use Aviat\Ion\View\HtmlView;
/**
@@ -109,13 +108,17 @@ class Index extends BaseController {
$username = $this->config->get(['kitsu_username']);
$model = $this->container->get('kitsu-model');
$data = $model->getUserData($username);
- $orgData = JsonAPI::organizeData($data);
+ $orgData = JsonAPI::organizeData($data)[0];
+ $rels = $orgData['relationships'] ?? [];
+ $favorites = array_key_exists('favorites', $rels) ? $rels['favorites'] : [];
+
+
$this->outputHTML('me', [
'title' => 'About ' . $this->config->get('whose_list'),
- 'data' => $orgData[0],
- 'attributes' => $orgData[0]['attributes'],
- 'relationships' => $orgData[0]['relationships'],
- 'favorites' => $this->organizeFavorites($orgData[0]['relationships']['favorites']),
+ 'data' => $orgData,
+ 'attributes' => $orgData['attributes'],
+ 'relationships' => $rels,
+ 'favorites' => $this->organizeFavorites($favorites),
]);
}
@@ -151,14 +154,14 @@ class Index extends BaseController {
return;
}
- $promise = (new Client)->request($kitsuUrl);
+ $promise = (new HummingbirdClient)->request($kitsuUrl);
$response = wait($promise);
- $data = (string) $response->getBody();
+ $data = wait($response->getBody());
$baseSavePath = $this->config->get('img_cache_path');
file_put_contents("{$baseSavePath}/{$type}/{$id}.{$ext}", $data);
header('Content-type: ' . $response->getHeader('content-type')[0]);
- echo (string) $response->getBody();
+ echo $data;
}
private function organizeFavorites(array $rawfavorites): array
diff --git a/src/Model/Anime.php b/src/Model/Anime.php
index eccc2b3f..43c18d35 100644
--- a/src/Model/Anime.php
+++ b/src/Model/Anime.php
@@ -158,9 +158,9 @@ class Anime extends API {
$requester->addRequest($this->kitsuModel->createListItem($data), 'kitsu');
- $results = $requester->makeRequests(TRUE);
+ $results = $requester->makeRequests();
- return count($results[1]) > 0;
+ return count($results) > 0;
}
/**
@@ -180,11 +180,13 @@ class Anime extends API {
$requester->addRequest($this->kitsuModel->updateListItem($data), 'kitsu');
- $results = $requester->makeRequests(TRUE);
+ $results = $requester->makeRequests();
+ $body = Json::decode($results['kitsu']);
+ $statusCode = (array_key_exists('error', $body)) ? 400: 200;
return [
- 'body' => Json::decode($results[1]['kitsu']->getBody()),
- 'statusCode' => $results[1]['kitsu']->getStatus()
+ 'body' => Json::decode($results['kitsu']),
+ 'statusCode' => $statusCode
];
}
@@ -206,8 +208,8 @@ class Anime extends API {
$requester->addRequest($this->kitsuModel->deleteListItem($id), 'kitsu');
- $results = $requester->makeRequests(TRUE);
+ $results = $requester->makeRequests();
- return count($results[1]) > 0;
+ return count($results) > 0;
}
}
\ No newline at end of file
diff --git a/src/Model/Manga.php b/src/Model/Manga.php
index 1fcad1a0..dbd5bc89 100644
--- a/src/Model/Manga.php
+++ b/src/Model/Manga.php
@@ -131,9 +131,9 @@ class Manga extends API
$requester->addRequest($this->kitsuModel->createListItem($data), 'kitsu');
- $results = $requester->makeRequests(TRUE);
+ $results = $requester->makeRequests();
- return count($results[1]) > 0;
+ return count($results) > 0;
}
/**
@@ -153,11 +153,13 @@ class Manga extends API
$requester->addRequest($this->kitsuModel->updateListItem($data), 'kitsu');
- $results = $requester->makeRequests(TRUE);
+ $results = $requester->makeRequests();
+ $body = Json::decode($results['kitsu']);
+ $statusCode = (array_key_exists('error', $body)) ? 400: 200;
return [
- 'body' => Json::decode($results[1]['kitsu']->getBody()),
- 'statusCode' => $results[1]['kitsu']->getStatus()
+ 'body' => Json::decode($results['kitsu']),
+ 'statusCode' => $statusCode
];
}
@@ -179,9 +181,9 @@ class Manga extends API
$requester->addRequest($this->kitsuModel->deleteListItem($id), 'kitsu');
- $results = $requester->makeRequests(TRUE);
+ $results = $requester->makeRequests();
- return count($results[1]) > 0;
+ return count($results) > 0;
}
/**
diff --git a/tests/API/APIRequestBuilderTest.php b/tests/API/APIRequestBuilderTest.php
index 6b92eacf..1f0020dc 100644
--- a/tests/API/APIRequestBuilderTest.php
+++ b/tests/API/APIRequestBuilderTest.php
@@ -16,15 +16,14 @@
namespace Aviat\AnimeClient\Tests\API;
-use Amp;
-use Amp\Artax\Client;
-use Aviat\AnimeClient\API\APIRequestBuilder;
+use function Amp\Promise\wait;
+use Aviat\AnimeClient\API\{APIRequestBuilder, HummingbirdClient};
use Aviat\Ion\Json;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
class APIRequestBuilderTest extends TestCase {
-
+
protected $builder;
public function setUp()
@@ -42,8 +41,8 @@ class APIRequestBuilderTest extends TestCase {
{
$request = $this->builder->newRequest('GET', 'gzip')
->getFullRequest();
- $response = Amp\wait((new Client)->request($request));
- $body = Json::decode($response->getBody());
+ $response = wait((new HummingbirdClient)->request($request));
+ $body = Json::decode(wait($response->getBody()));
$this->assertEquals(1, $body['gzipped']);
}
@@ -60,8 +59,8 @@ class APIRequestBuilderTest extends TestCase {
->setBasicAuth('username', 'password')
->getFullRequest();
- $response = Amp\wait((new Client)->request($request));
- $body = Json::decode($response->getBody());
+ $response = wait((new HummingbirdClient)->request($request));
+ $body = Json::decode(wait($response->getBody()));
$this->assertEquals('Basic dXNlcm5hbWU6cGFzc3dvcmQ=', $body['headers']['Authorization']);
}
@@ -88,8 +87,8 @@ class APIRequestBuilderTest extends TestCase {
->setQuery($query)
->getFullRequest();
- $response = Amp\wait((new Client)->request($request));
- $body = Json::decode($response->getBody());
+ $response = wait((new HummingbirdClient)->request($request));
+ $body = Json::decode(wait($response->getBody()));
$this->assertEquals($expected, $body['args']);
}
@@ -105,8 +104,8 @@ class APIRequestBuilderTest extends TestCase {
->setFormFields($formValues)
->getFullRequest();
- $response = Amp\wait((new Client)->request($request));
- $body = Json::decode($response->getBody());
+ $response = wait((new HummingbirdClient)->request($request));
+ $body = Json::decode(wait($response->getBody()));
$this->assertEquals($formValues, $body['form']);
}
@@ -129,8 +128,8 @@ class APIRequestBuilderTest extends TestCase {
->setJsonBody($data)
->getFullRequest();
- $response = Amp\wait((new Client)->request($request));
- $body = Json::decode($response->getBody());
+ $response = wait((new HummingbirdClient)->request($request));
+ $body = Json::decode(wait($response->getBody()));
$this->assertEquals($data, $body['json']);
}
diff --git a/tests/API/MAL/MALTraitTest.php b/tests/API/MAL/MALTraitTest.php
index 10ec1151..5c239eb9 100644
--- a/tests/API/MAL/MALTraitTest.php
+++ b/tests/API/MAL/MALTraitTest.php
@@ -46,6 +46,5 @@ class MALTraitTest extends AnimeClientTestCase {
]);
$this->assertInstanceOf(\Amp\Artax\Request::class, $request);
$this->assertEquals($request->getUri(), 'https://myanimelist.net/api/foo?foo=bar');
- $this->assertEquals($request->getBody(), '');
}
}
\ No newline at end of file
diff --git a/tests/AnimeClientTestCase.php b/tests/AnimeClientTestCase.php
index 7c4e0804..4418e787 100644
--- a/tests/AnimeClientTestCase.php
+++ b/tests/AnimeClientTestCase.php
@@ -53,9 +53,9 @@ class AnimeClientTestCase extends TestCase {
public static function setUpBeforeClass()
{
// Use mock session handler
- $session_handler = new TestSessionHandler();
- session_set_save_handler($session_handler, TRUE);
- self::$session_handler = $session_handler;
+ //$session_handler = new TestSessionHandler();
+ //session_set_save_handler($session_handler, TRUE);
+ //self::$session_handler = $session_handler;
// Remove test cache files
$files = glob(_dir(TEST_DATA_DIR, 'cache', '*.json'));