diff --git a/src/AnimeClient/API/Kitsu/Auth.php b/src/AnimeClient/API/Kitsu/Auth.php index 1a36b2a2..729aa5a7 100644 --- a/src/AnimeClient/API/Kitsu/Auth.php +++ b/src/AnimeClient/API/Kitsu/Auth.php @@ -27,6 +27,7 @@ use Aviat\AnimeClient\API\{ }; use Aviat\Ion\Di\{ContainerAware, ContainerInterface}; use Aviat\Ion\Di\Exception\{ContainerException, NotFoundException}; +use Aviat\Ion\Event; use Throwable; use const PHP_SAPI; @@ -66,6 +67,8 @@ final class Auth { $this->segment = $container->get('session') ->getSegment(SESSION_SEGMENT); $this->model = $container->get('kitsu-model'); + + Event::on('::unauthorized::', [$this, 'reAuthenticate']); } /** @@ -87,6 +90,28 @@ final class Auth { return $this->storeAuth($auth); } + /** + * Make the call to re-authenticate with the existing refresh token + * + * @param string $refreshToken + * @return boolean + * @throws InvalidArgumentException + * @throws Throwable + */ + public function reAuthenticate(?string $refreshToken): bool + { + $refreshToken ??= $this->getAuthToken(); + + if (empty($refreshToken)) + { + return FALSE; + } + + $auth = $this->model->reAuthenticate($refreshToken); + + return $this->storeAuth($auth); + } + /** * Check whether the current user is authenticated * @@ -110,7 +135,7 @@ final class Auth { /** * Retrieve the authentication token from the session * - * @return string|false + * @return string */ private function getAuthToken(): ?string { @@ -146,22 +171,14 @@ final class Auth { return $token; } - /** - * Make the call to re-authenticate with the existing refresh token - * - * @param string $token - * @return boolean - * @throws InvalidArgumentException - * @throws Throwable - */ - private function reAuthenticate(string $token): bool + private function getRefreshToken(): ?string { - $auth = $this->model->reAuthenticate($token); - - return $this->storeAuth($auth); + return (PHP_SAPI === 'cli') + ? $this->cacheGet(K::AUTH_TOKEN_REFRESH_CACHE_KEY, NULL) + : $this->segment->get('refresh_token'); } - private function storeAuth($auth): bool + private function storeAuth(bool $auth): bool { if (FALSE !== $auth) { diff --git a/src/AnimeClient/API/Kitsu/KitsuTrait.php b/src/AnimeClient/API/Kitsu/KitsuTrait.php index 1041c3c2..67ce2815 100644 --- a/src/AnimeClient/API/Kitsu/KitsuTrait.php +++ b/src/AnimeClient/API/Kitsu/KitsuTrait.php @@ -16,6 +16,8 @@ namespace Aviat\AnimeClient\API\Kitsu; +use Aviat\AnimeClient\Enum\EventType; +use function in_array; use const PHP_SAPI; use const Aviat\AnimeClient\SESSION_SEGMENT; @@ -24,10 +26,8 @@ use function Aviat\AnimeClient\getResponse; use Amp\Http\Client\Request; use Amp\Http\Client\Response; -use Aviat\AnimeClient\API\{ - FailedResponseException, - Kitsu as K -}; +use Aviat\AnimeClient\API\{FailedResponseException, Kitsu as K}; +use Aviat\Ion\Event; use Aviat\Ion\Json; use Aviat\Ion\JsonException; @@ -80,7 +80,7 @@ trait KitsuTrait { else if ($url !== K::AUTH_URL && $sessionSegment->get('auth_token') !== NULL) { $token = $sessionSegment->get('auth_token'); - if ( ! $cacheItem->isHit()) + if ( ! (empty($token) || $cacheItem->isHit())) { $cacheItem->set($token); $cacheItem->save(); @@ -168,12 +168,20 @@ trait KitsuTrait { } $response = $this->getResponse($type, $url, $options); + $statusCode = $response->getStatus(); - if ((int) $response->getStatus() > 299 || (int) $response->getStatus() < 200) + // Check for requests that are unauthorized + if ($statusCode === 401 || $statusCode === 403) + { + Event::emit(EventType::UNAUTHORIZED); + } + + // Any other type of failed request + if ($statusCode > 299 || $statusCode < 200) { if ($logger) { - $logger->warning('Non 200 response for api call', (array)$response); + $logger->warning('Non 2xx response for api call', (array)$response); } throw new FailedResponseException('Failed to get the proper response from the API'); @@ -188,7 +196,6 @@ trait KitsuTrait { print_r($e); die(); } - } /** @@ -233,12 +240,9 @@ trait KitsuTrait { $response = $this->getResponse('POST', ...$args); $validResponseCodes = [200, 201]; - if ( ! \in_array((int) $response->getStatus(), $validResponseCodes, TRUE)) + if ( ! in_array($response->getStatus(), $validResponseCodes, TRUE) && $logger) { - if ($logger) - { - $logger->warning('Non 201 response for POST api call', $response->getBody()); - } + $logger->warning('Non 201 response for POST api call', $response->getBody()); } return JSON::decode(wait($response->getBody()->buffer()), TRUE); @@ -254,6 +258,6 @@ trait KitsuTrait { protected function deleteRequest(...$args): bool { $response = $this->getResponse('DELETE', ...$args); - return ((int) $response->getStatus() === 204); + return ($response->getStatus() === 204); } } \ No newline at end of file diff --git a/src/AnimeClient/API/Kitsu/Model.php b/src/AnimeClient/API/Kitsu/Model.php index 15c5687d..ad02b958 100644 --- a/src/AnimeClient/API/Kitsu/Model.php +++ b/src/AnimeClient/API/Kitsu/Model.php @@ -582,7 +582,7 @@ final class Model { { $defaultOptions = [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => 'anime' ], 'page' => [ @@ -608,7 +608,7 @@ final class Model { { $options = [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => 'anime', 'status' => $status, ], @@ -683,7 +683,7 @@ final class Model { $options = [ 'query' => [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => 'manga', 'status' => $status, ], @@ -811,7 +811,7 @@ final class Model { { $defaultOptions = [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => 'manga' ], 'page' => [ @@ -866,7 +866,7 @@ final class Model { */ public function createListItem(array $data): ?Request { - $data['user_id'] = $this->getUserIdByUsername($this->getUsername()); + $data['user_id'] = $this->getUserId(); if ($data['id'] === NULL) { return NULL; @@ -941,7 +941,7 @@ final class Model { { $options = [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => $type, ], 'include' => "{$type},{$type}.mappings", @@ -1001,7 +1001,7 @@ final class Model { 'query' => [ 'filter' => [ 'kind' => 'progressed,updated', - 'userId' => $this->getUserIdByUsername($this->getUsername()), + 'userId' => $this->getUserId(), ], 'page' => [ 'offset' => $offset, @@ -1018,6 +1018,18 @@ final class Model { ]); } + private function getUserId(): string + { + static $userId = NULL; + + if ($userId === NULL) + { + $userId = $this->getUserIdByUsername($this->getUsername()); + } + + return $userId; + } + /** * Get the kitsu username from config * @@ -1105,7 +1117,7 @@ final class Model { $options = [ 'query' => [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername(), + 'user_id' => $this->getUserId(), 'kind' => $type, ], 'page' => [ @@ -1175,7 +1187,7 @@ final class Model { { $defaultOptions = [ 'filter' => [ - 'user_id' => $this->getUserIdByUsername($this->getUsername()), + 'user_id' => $this->getUserId(), 'kind' => $type, ], 'page' => [ diff --git a/src/AnimeClient/Controller.php b/src/AnimeClient/Controller.php index 46ae7e4f..5fc6c034 100644 --- a/src/AnimeClient/Controller.php +++ b/src/AnimeClient/Controller.php @@ -16,6 +16,7 @@ namespace Aviat\AnimeClient; +use Aviat\AnimeClient\Enum\EventType; use function Aviat\Ion\_dir; use Aura\Router\Generator; @@ -32,6 +33,7 @@ use Aviat\Ion\Di\{ Exception\ContainerException, Exception\NotFoundException }; +use Aviat\Ion\Event; use Aviat\Ion\Exception\DoubleRenderException; use Aviat\Ion\View\{HtmlView, HttpView, JsonView}; use InvalidArgumentException; @@ -131,6 +133,9 @@ class Controller { 'url_type' => 'anime', 'urlGenerator' => $urlGenerator, ]; + + Event::on(EventType::CLEAR_CACHE, fn () => $this->emptyCache()); + Event::on(EventType::RESET_CACHE_KEY, fn (string $key) => $this->removeCacheItem($key)); } /** @@ -430,5 +435,15 @@ class Controller { (new HttpView($this->container))->redirect($url, $code); exit(); } + + private function emptyCache(): void + { + $this->cache->emptyCache(); + } + + private function removeCacheItem(string $key): void + { + $this->cache->deleteItem($key); + } } // End of BaseController.php \ No newline at end of file diff --git a/src/AnimeClient/Controller/Misc.php b/src/AnimeClient/Controller/Misc.php index 035248f4..0e5a5d42 100644 --- a/src/AnimeClient/Controller/Misc.php +++ b/src/AnimeClient/Controller/Misc.php @@ -17,6 +17,8 @@ namespace Aviat\AnimeClient\Controller; use Aviat\AnimeClient\Controller as BaseController; +use Aviat\AnimeClient\Enum\EventType; +use Aviat\Ion\Event; use Aviat\Ion\View\HtmlView; /** @@ -30,7 +32,10 @@ final class Misc extends BaseController { */ public function clearCache(): void { - $this->cache->clear(); + $this->checkAuth(); + + Event::emit(EventType::CLEAR_CACHE); + $this->outputHTML('blank', [ 'title' => 'Cache cleared' ]); @@ -89,8 +94,6 @@ final class Misc extends BaseController { */ public function logout(): void { - $this->checkAuth(); - $auth = $this->container->get('auth'); $auth->logout(); diff --git a/src/AnimeClient/Dispatcher.php b/src/AnimeClient/Dispatcher.php index ecb650a3..5d58b413 100644 --- a/src/AnimeClient/Dispatcher.php +++ b/src/AnimeClient/Dispatcher.php @@ -16,12 +16,14 @@ namespace Aviat\AnimeClient; +use Aviat\AnimeClient\Enum\EventType; use function Aviat\Ion\_dir; use Aura\Router\{Map, Matcher, Route, Rule}; use Aviat\AnimeClient\API\FailedResponseException; use Aviat\Ion\Di\ContainerInterface; +use Aviat\Ion\Event; use Aviat\Ion\Friend; use Aviat\Ion\Type\StringType; use LogicException; @@ -161,10 +163,7 @@ final class Dispatcher extends RoutingBase { throw new LogicException('Missing controller'); } - if (array_key_exists('controller', $route->attributes)) - { - $controllerName = $route->attributes['controller']; - } + $controllerName = $route->attributes['controller']; // Get the full namespace for a controller if a short name is given if (strpos($controllerName, '\\') === FALSE) @@ -283,7 +282,7 @@ final class Dispatcher extends RoutingBase { $logger->debug('Dispatcher - controller arguments', $params); } - \call_user_func_array([$controller, $method], $params); + call_user_func_array([$controller, $method], $params); } catch (FailedResponseException $e) { @@ -293,7 +292,14 @@ final class Dispatcher extends RoutingBase { 'API request timed out', 'Failed to retrieve data from API (╯°□°)╯︵ ┻━┻'); } - + finally + { + // Log out on session/api token expiration + Event::on(EventType::UNAUTHORIZED, static function () { + $controllerName = DEFAULT_CONTROLLER; + (new $controllerName($this->container))->logout(); + }); + } } /** diff --git a/src/AnimeClient/Enum/EventType.php b/src/AnimeClient/Enum/EventType.php new file mode 100644 index 00000000..5edf0c9f --- /dev/null +++ b/src/AnimeClient/Enum/EventType.php @@ -0,0 +1,25 @@ + + * @copyright 2015 - 2020 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 5 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\AnimeClient\Enum; + +use Aviat\Ion\Enum as BaseEnum; + +final class EventType extends BaseEnum { + public const CLEAR_CACHE = '::clear-cache::'; + public const RESET_CACHE_KEY = '::reset-cache-key::'; + public const UNAUTHORIZED = '::unauthorized::'; +} \ No newline at end of file diff --git a/src/Ion/Event.php b/src/Ion/Event.php new file mode 100644 index 00000000..9cf226d7 --- /dev/null +++ b/src/Ion/Event.php @@ -0,0 +1,55 @@ + + * @copyright 2015 - 2020 Timothy J. Warren + * @license http://www.opensource.org/licenses/mit-license.html MIT License + * @version 5 + * @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient + */ + +namespace Aviat\Ion; + +/** + * A basic event handler + */ +class Event { + private static array $eventMap = []; + + /** + * Subscribe to an event + * + * @param string $eventName + * @param callable $handler + */ + public static function on(string $eventName, callable $handler): void + { + if ( ! array_key_exists($eventName, static::$eventMap)) + { + static::$eventMap[$eventName] = []; + } + + static::$eventMap[$eventName][] = $handler; + } + + /** + * Fire off an event + * + * @param string $eventName + * @param array $args + */ + public static function emit(string $eventName, array $args = []): void + { + // Call each subscriber with the provided arguments + if (array_key_exists($eventName, static::$eventMap)) + { + array_walk(static::$eventMap[$eventName], fn ($fn) => $fn(...$args)); + } + } +} \ No newline at end of file