diff --git a/composer.json b/composer.json index f0d31d4..dcd7b17 100755 --- a/composer.json +++ b/composer.json @@ -11,13 +11,9 @@ "authors": [ { "name": "Sebastian Bürgin-Fix", - "email": "sebastian.buergin@buergin.ch", + "email": "helpdesk@codebar.ch", "homepage": "https://www.codebar.ch", "role": "Software-Engineer" - }, - { - "name": "Rhys Lees", - "role": "Software-Engineer" } ], "require": { @@ -26,9 +22,9 @@ "illuminate/contracts": "^12.0", "nesbot/carbon": "^3.8", "spatie/laravel-package-tools": "^1.19", - "saloonphp/cache-plugin": "^3.0", - "saloonphp/laravel-plugin": "^3.8", - "saloonphp/saloon": "^3.14" + "saloonphp/cache-plugin": "^3.1", + "saloonphp/laravel-plugin": "^3.0|^4.0", + "saloonphp/saloon": "^3.0|^4.0" }, "require-dev": { "laravel/pint": "^1.21", diff --git a/src/Actions/InstagramHandler.php b/src/Actions/InstagramHandler.php index ca5e437..0fbd6da 100755 --- a/src/Actions/InstagramHandler.php +++ b/src/Actions/InstagramHandler.php @@ -28,15 +28,15 @@ public static function connector(): InstagramConnector throw new \Exception('No authenticator found. Please authenticate first.'); } - $authenticator = InstagramAuthenticator::unserialize($serialized); + $authenticator = InstagramAuthenticator::decodeFromCache($serialized); $connector = new InstagramConnector; if ($authenticator->hasExpired()) { $authenticator = $connector->refreshAccessToken($authenticator); - // @phpstan-ignore-next-line - Cache::store(config('instagram.cache_store'))->put('instagram.authenticator', $authenticator->serialize(), now()->addDays(60)); + assert($authenticator instanceof InstagramAuthenticator); + Cache::store(config('instagram.cache_store'))->put('instagram.authenticator', $authenticator->encodeForCache(), now()->addDays(60)); } $connector->authenticate($authenticator); diff --git a/src/Authenticator/InstagramAuthenticator.php b/src/Authenticator/InstagramAuthenticator.php index 2a7bb5f..85b3eb5 100755 --- a/src/Authenticator/InstagramAuthenticator.php +++ b/src/Authenticator/InstagramAuthenticator.php @@ -4,6 +4,7 @@ use DateTimeImmutable; use Illuminate\Support\Carbon; +use JsonException; use Saloon\Contracts\OAuthAuthenticator; use Saloon\Http\PendingRequest; @@ -88,18 +89,52 @@ public function isNotRefreshable(): bool } /** - * Serialize the access token. + * Encode for cache storage (JSON). Replaces PHP serialize, which is unsafe and unsupported with Saloon v4+. + * + * @throws JsonException */ - public function serialize(): string + public function encodeForCache(): string { - return serialize($this); + return json_encode([ + 'accessToken' => $this->accessToken, + 'refreshToken' => $this->refreshToken, + 'expiresAt' => $this->expiresAt?->format(DATE_ATOM), + ], JSON_THROW_ON_ERROR); } /** - * Unserialize the access token. + * Restore from cache. Supports JSON (current) and legacy PHP-serialized payloads for one-time migration. + * + * @throws JsonException */ - public static function unserialize(string $string): static + public static function decodeFromCache(string $payload): static { - return unserialize($string, ['allowed_classes' => true]); + $trimmed = ltrim($payload); + + if ($trimmed !== '' && $trimmed[0] === '{') { + $data = json_decode($payload, true, 512, JSON_THROW_ON_ERROR); + $expiresAt = isset($data['expiresAt']) && is_string($data['expiresAt']) && $data['expiresAt'] !== '' + ? new DateTimeImmutable($data['expiresAt']) + : null; + + return new static( + $data['accessToken'], + $data['refreshToken'] ?? null, + $expiresAt, + ); + } + + $legacy = unserialize($payload, [ + 'allowed_classes' => [ + static::class, + DateTimeImmutable::class, + ], + ]); + + if (! $legacy instanceof static) { + throw new \InvalidArgumentException('Invalid cached Instagram authenticator payload.'); + } + + return $legacy; } } diff --git a/src/Connectors/InstagramConnector.php b/src/Connectors/InstagramConnector.php index 47e553f..c6f0a9c 100755 --- a/src/Connectors/InstagramConnector.php +++ b/src/Connectors/InstagramConnector.php @@ -53,6 +53,7 @@ protected function defaultOauthConfig(): OAuthConfig ->setRedirectUri(route('instagram.callback')) ->setAuthorizeEndpoint('https://www.instagram.com/oauth/authorize') ->setTokenEndpoint('https://api.instagram.com/oauth/access_token') + ->setAllowBaseUrlOverride() ->setUserEndpoint('/me'); } } diff --git a/src/Http/Controllers/InstagramController.php b/src/Http/Controllers/InstagramController.php index d1b3a55..5997e94 100755 --- a/src/Http/Controllers/InstagramController.php +++ b/src/Http/Controllers/InstagramController.php @@ -3,6 +3,7 @@ namespace CodebarAg\LaravelInstagram\Http\Controllers; use CodebarAg\LaravelInstagram\Actions\InstagramHandler; +use CodebarAg\LaravelInstagram\Authenticator\InstagramAuthenticator; use CodebarAg\LaravelInstagram\Connectors\InstagramConnector; use CodebarAg\LaravelInstagram\Requests\GetInstagramMe; use Illuminate\Http\Request; @@ -35,9 +36,10 @@ public function callback(Request $request) $connector = new InstagramConnector; $shortLivedAuthenticator = $connector->getShortLivedAccessToken(code: $request->query->get('code')); $authenticator = $connector->getAccessToken(code: $shortLivedAuthenticator->accessToken); // @phpstan-ignore-line - $serialized = $authenticator->serialize(); // @phpstan-ignore-line + assert($authenticator instanceof InstagramAuthenticator); + $cachePayload = $authenticator->encodeForCache(); - Cache::store(config('instagram.cache_store'))->put('instagram.authenticator', $serialized, now()->addDays(60)); + Cache::store(config('instagram.cache_store'))->put('instagram.authenticator', $cachePayload, now()->addDays(60)); $connector = InstagramHandler::connector(); $request = new GetInstagramMe; diff --git a/src/Requests/Authentication/GetAccessTokenRequest.php b/src/Requests/Authentication/GetAccessTokenRequest.php index 7d6e21f..853cde2 100755 --- a/src/Requests/Authentication/GetAccessTokenRequest.php +++ b/src/Requests/Authentication/GetAccessTokenRequest.php @@ -13,6 +13,8 @@ class GetAccessTokenRequest extends Request { use AcceptsJson; + public ?bool $allowBaseUrlOverride = true; + /** * Define the method that the request will use. */ diff --git a/src/Requests/Authentication/GetRefreshAccessTokenRequest.php b/src/Requests/Authentication/GetRefreshAccessTokenRequest.php index d3ef8bc..23231a8 100755 --- a/src/Requests/Authentication/GetRefreshAccessTokenRequest.php +++ b/src/Requests/Authentication/GetRefreshAccessTokenRequest.php @@ -12,6 +12,8 @@ class GetRefreshAccessTokenRequest extends Request { use AcceptsJson; + public ?bool $allowBaseUrlOverride = true; + /** * Define the method that the request will use. */ diff --git a/src/Requests/Authentication/GetShortLivedAccessTokenRequest.php b/src/Requests/Authentication/GetShortLivedAccessTokenRequest.php index f0492aa..30504d5 100755 --- a/src/Requests/Authentication/GetShortLivedAccessTokenRequest.php +++ b/src/Requests/Authentication/GetShortLivedAccessTokenRequest.php @@ -16,6 +16,8 @@ class GetShortLivedAccessTokenRequest extends Request implements HasBody use AcceptsJson; use HasFormBody; + public ?bool $allowBaseUrlOverride = true; + /** * Define the method that the request will use. */ diff --git a/src/Traits/AuthorizationCodeGrant.php b/src/Traits/AuthorizationCodeGrant.php index 4713c54..ddbbf26 100755 --- a/src/Traits/AuthorizationCodeGrant.php +++ b/src/Traits/AuthorizationCodeGrant.php @@ -61,7 +61,7 @@ public function getAuthorizationUrl(array $scopes = [], ?string $state = null, s $query = http_build_query($queryParameters, '', '&', PHP_QUERY_RFC3986); $query = trim($query, '?&'); - $url = URLHelper::join($this->resolveBaseUrl(), $config->getAuthorizeEndpoint()); + $url = URLHelper::join($this->resolveBaseUrl(), $config->getAuthorizeEndpoint(), $config->getAllowBaseUrlOverride()); $glue = str_contains($url, '?') ? '&' : '?';