Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -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",
Expand Down
6 changes: 3 additions & 3 deletions src/Actions/InstagramHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Comment on lines +31 to 32
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decodeFromCache can now throw (e.g., JsonException/InvalidArgumentException) when the cached payload is corrupted. Consider catching decode failures here, clearing the cache key (similar to the empty($serialized) branch), and throwing the same "No authenticator found" exception to avoid hard failures from a single bad cache value.

Suggested change
$authenticator = InstagramAuthenticator::decodeFromCache($serialized);
try {
$authenticator = InstagramAuthenticator::decodeFromCache($serialized);
} catch (\Throwable $e) {
Cache::store(config('instagram.cache_store'))->forget('instagram.authenticator');
throw new \Exception('No authenticator found. Please authenticate first.');
}

Copilot uses AI. Check for mistakes.
$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);
Expand Down
47 changes: 41 additions & 6 deletions src/Authenticator/InstagramAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use DateTimeImmutable;
use Illuminate\Support\Carbon;
use JsonException;
use Saloon\Contracts\OAuthAuthenticator;
use Saloon\Http\PendingRequest;

Expand Down Expand Up @@ -88,18 +89,52 @@
}

/**
* 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
Comment on lines +92 to +110
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

encodeForCache/decodeFromCache replace the previously public serialize/unserialize API. If this package has external consumers, this is a breaking change; consider keeping serialize/unserialize as deprecated wrappers (or documenting/bumping a major version) to avoid unexpected runtime errors for downstream users.

Copilot uses AI. Check for mistakes.
{
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;

Comment on lines +116 to +119
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decodeFromCache only documents @throws JsonException, but new DateTimeImmutable($data['expiresAt']) can also throw (e.g., invalid timestamp format). Either validate/catch and rethrow a consistent exception type, or update the PHPDoc to reflect the additional exception(s) so callers can handle corrupted cache payloads predictably.

Suggested change
$expiresAt = isset($data['expiresAt']) && is_string($data['expiresAt']) && $data['expiresAt'] !== ''
? new DateTimeImmutable($data['expiresAt'])
: null;
$expiresAt = null;
if (isset($data['expiresAt']) && is_string($data['expiresAt']) && $data['expiresAt'] !== '') {
try {
$expiresAt = new DateTimeImmutable($data['expiresAt']);
} catch (\Exception $e) {
throw new JsonException('Invalid expiresAt value in cached Instagram authenticator payload.', 0, $e);
}
}

Copilot uses AI. Check for mistakes.
return new static(

Check failure on line 120 in src/Authenticator/InstagramAuthenticator.php

View workflow job for this annotation

GitHub Actions / phpstan

Unsafe usage of new static().

Check failure on line 120 in src/Authenticator/InstagramAuthenticator.php

View workflow job for this annotation

GitHub Actions / phpstan

Unsafe usage of new static().
$data['accessToken'],
$data['refreshToken'] ?? null,
$expiresAt,
);
Comment on lines +114 to +124
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the JSON branch, $data['accessToken'] is used without checking it exists/is a string. A corrupted/partial cache entry will trigger notices/type errors rather than a controlled exception. Add explicit validation for required keys/types (and throw an InvalidArgumentException if invalid) before constructing the authenticator.

Copilot uses AI. Check for mistakes.
}
Comment on lines +96 to +125
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new cache encoding/decoding logic (JSON + legacy PHP-serialized migration path) isn't covered by tests. Add tests for encodeForCache/decodeFromCache round-tripping, and for successfully reading a legacy serialized payload, to prevent regressions during the Saloon v4 migration.

Copilot uses AI. Check for mistakes.

$legacy = unserialize($payload, [
'allowed_classes' => [
static::class,
DateTimeImmutable::class,
],
]);

if (! $legacy instanceof static) {
throw new \InvalidArgumentException('Invalid cached Instagram authenticator payload.');
}

return $legacy;
}
}
1 change: 1 addition & 0 deletions src/Connectors/InstagramConnector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
}
6 changes: 4 additions & 2 deletions src/Http/Controllers/InstagramController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/Requests/Authentication/GetAccessTokenRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class GetAccessTokenRequest extends Request
{
use AcceptsJson;

public ?bool $allowBaseUrlOverride = true;

/**
* Define the method that the request will use.
*/
Expand Down
2 changes: 2 additions & 0 deletions src/Requests/Authentication/GetRefreshAccessTokenRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ class GetRefreshAccessTokenRequest extends Request
{
use AcceptsJson;

public ?bool $allowBaseUrlOverride = true;

/**
* Define the method that the request will use.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Traits/AuthorizationCodeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, '?') ? '&' : '?';

Expand Down
Loading