From 970b04f28e9d690fc2c74843eeda626c3e17b4b3 Mon Sep 17 00:00:00 2001 From: Chris Page Date: Fri, 23 Jan 2026 13:42:38 +0000 Subject: [PATCH 1/7] Updated testing Rolled back on adding an additional constructor to ensure backwards compatibility --- src/BunnyCDNAdapter.php | 36 ++++++++++++++++++++------------ tests/FlysystemAdapterTest.php | 19 ----------------- tests/PrefixTest.php | 12 ----------- tests/TemporaryUrlTest.php | 38 ++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 44 deletions(-) create mode 100644 tests/TemporaryUrlTest.php diff --git a/src/BunnyCDNAdapter.php b/src/BunnyCDNAdapter.php index fc25cbd..ba7b9e0 100644 --- a/src/BunnyCDNAdapter.php +++ b/src/BunnyCDNAdapter.php @@ -2,7 +2,6 @@ namespace PlatformCommunity\Flysystem\BunnyCDN; -use Carbon\CarbonInterface; use DateTimeInterface; use Exception; use GuzzleHttp\Exception\RequestException; @@ -29,8 +28,9 @@ use League\Flysystem\UnableToSetVisibility; use League\Flysystem\UnableToWriteFile; use League\Flysystem\UrlGeneration\PublicUrlGenerator; -use League\Flysystem\Visibility; use League\Flysystem\UrlGeneration\TemporaryUrlGenerator; +use League\Flysystem\UnableToGenerateTemporaryUrl; +use League\Flysystem\Visibility; use League\MimeTypeDetection\FinfoMimeTypeDetector; use PlatformCommunity\Flysystem\BunnyCDN\Exceptions\NotFoundException; use RuntimeException; @@ -40,17 +40,27 @@ class BunnyCDNAdapter implements FilesystemAdapter, PublicUrlGenerator, Checksum { use CalculateChecksumFromStream; + private string $token_auth_key = ''; + public function __construct( private BunnyCDNClient $client, private string $pullzone_url = '', - private string $token_auth_key = '' - ) - { + ) { if (\func_num_args() > 2 && (string) \func_get_arg(2) !== '') { throw new \RuntimeException('PrefixPath is no longer supported directly. Use PathPrefixedAdapter instead: https://flysystem.thephpleague.com/docs/adapter/path-prefixing/'); } } + /** + * Set the token auth key for generating temporaryUrls. + */ + public function setTokenAuthKey(string $tokenAuthKey): BunnyCDNAdapter + { + $this->token_auth_key = $tokenAuthKey; + + return $this; + } + /** * @param $source * @param $destination @@ -564,7 +574,7 @@ public function publicUrl(string $path, Config $config): string public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string { if ($this->token_auth_key === '') { - throw new RuntimeException('In order to generate temporary URLs for a BunnyCDN object, you must pass the "token_auth_key" parameter to the BunnyCDNAdapter.'); + throw new UnableToGenerateTemporaryUrl('In order to generate temporary URLs for a BunnyCDN object, you must call the `setTokenAuthKey` method on the BunnyCDNAdapter.', $path); } // convert our expiration to a unix timestamp @@ -580,22 +590,22 @@ public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config // concatenate all of our data return $path - . (str_contains($path, '?') ? '&' : '?') - . 'token=' . $this->buildSigningKey($path, $expiration, $params) - . '&expires=' . $expiration - . ($params ? '&' . http_build_query($params) : null); + .(str_contains($path, '?') ? '&' : '?') + .'token='.$this->buildSigningKey($path, $expiration, $params) + .'&expires='.$expiration + .($params ? '&'.http_build_query($params) : null); } private function buildSigningKey($path, int $expiration, array $params): string { // prefix our path - $path = str_starts_with($path, '/') ? $path : '/' . $path; + $path = str_starts_with($path, '/') ? $path : '/'.$path; // process our query params - $query = implode('&', array_map(fn($k, $v) => $k . '=' . $v, array_keys($params), $params)); + $query = implode('&', array_map(fn ($k, $v) => $k.'='.$v, array_keys($params), $params)); // now generate and hash our payload - $payload = $this->token_auth_key . $path . (string)$expiration . $query; + $payload = $this->token_auth_key.$path.(string) $expiration.$query; $hash = hash('sha256', $payload, true); // sanitise and base64 encode it diff --git a/tests/FlysystemAdapterTest.php b/tests/FlysystemAdapterTest.php index e43e093..3de96b6 100644 --- a/tests/FlysystemAdapterTest.php +++ b/tests/FlysystemAdapterTest.php @@ -102,25 +102,6 @@ public function setting_visibility(): void $this->markTestSkipped('No visibility support is provided for BunnyCDN'); } - public function generating_a_temporary_url(): void - { - $adapter = new BunnyCDNAdapter(self::bunnyCDNClient(), '', 'test-key'); - - $expiresAt = new \DateTimeImmutable('+1 hour'); - $url = $adapter->temporaryUrl('path.txt', $expiresAt, new Config()); - - $this->assertStringContainsString('path.txt?token=', $url); - $this->assertStringContainsString('&expires=', $url); - } - - public function test_temporary_url_throws_exception_if_not_configured(): void - { - $this->expectException(\RuntimeException::class); - $this->expectExceptionMessage('In order to generate temporary URLs for a BunnyCDN object, you must pass the "token_auth_key" parameter to the BunnyCDNAdapter.'); - - $this->adapter()->temporaryUrl('path.txt', new \DateTimeImmutable('+1 hour'), new Config()); - } - /** * @test */ diff --git a/tests/PrefixTest.php b/tests/PrefixTest.php index 3d94601..406555d 100644 --- a/tests/PrefixTest.php +++ b/tests/PrefixTest.php @@ -65,18 +65,6 @@ public function setting_visibility(): void $this->markTestSkipped('No visibility support is provided for BunnyCDN'); } - public function generating_a_temporary_url(): void - { - $adapter = new BunnyCDNAdapter(self::bunnyCDNClient(), '', 'test-key'); - $prefixAdapter = new PathPrefixedAdapter($adapter, self::PREFIX_PATH); - - $expiresAt = new \DateTimeImmutable('+1 hour'); - $url = $prefixAdapter->temporaryUrl('path.txt', $expiresAt, new Config()); - - $this->assertStringContainsString(self::PREFIX_PATH . '/path.txt?token=', $url); - $this->assertStringContainsString('&expires=', $url); - } - /** * Overwritten (usually because of visibility) */ diff --git a/tests/TemporaryUrlTest.php b/tests/TemporaryUrlTest.php new file mode 100644 index 0000000..c8759cc --- /dev/null +++ b/tests/TemporaryUrlTest.php @@ -0,0 +1,38 @@ +expectException(UnableToGenerateTemporaryUrl::class); + $this->expectExceptionMessage('you must call the `setTokenAuthKey`'); + + $client = new BunnyCDNClient('test', 'test'); + $adapter = new BunnyCDNAdapter($client, 'pz-key'); + + $expiresAt = new \DateTimeImmutable('+1 hour'); + $adapter->temporaryUrl('testing.text', $expiresAt, new Config()); + } + + public function test_it_can_generate_signing_key() + { + $client = new BunnyCDNClient('test', 'test'); + $adapter = new BunnyCDNAdapter($client, 'pz-key'); + $adapter->setTokenAuthKey('test-auth-key'); + + $expiresAt = new \DateTimeImmutable('+1 hour'); + $url = $adapter->temporaryUrl('testing.txt', $expiresAt, new Config()); + + $this->assertStringContainsString('testing.txt?token=', $url); + $this->assertStringContainsString('expires=' . $expiresAt->getTimestamp(), $url); + } +} \ No newline at end of file From ab0f665a501f6981f5d9ddced3eebb3ebbcab626 Mon Sep 17 00:00:00 2001 From: Chris Page Date: Fri, 23 Jan 2026 14:19:53 +0000 Subject: [PATCH 2/7] Updated README --- readme.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index b98e4af..3a57ecf 100644 --- a/readme.md +++ b/readme.md @@ -28,6 +28,9 @@ $adapter = new BunnyCDNAdapter( ) ); +// for temporary URL support, define a signing key +$adapter->setTokenAuthKey('token-auth-signing-key'); + $filesystem = new Filesystem($adapter); ``` @@ -45,6 +48,10 @@ $adapter = new BunnyCDNAdapter( ), 'https://testing.b-cdn.net/' # Pull Zone URL ); + +// for temporary URL support, define a signing key +$adapter->setTokenAuthKey('token-auth-signing-key'); + $filesystem = new Filesystem($adapter); ``` @@ -52,7 +59,8 @@ _Note: You can also use your own domain name if it's configured in the pull zone Once you add your pull zone, you can use the `->getUrl($path)`, or in Laravel, the `->url($path)` command to get the fully qualified public URL of your BunnyCDN assets. -## Usage in Laravel 9 +## Usage in Laravel 9 & up + To add BunnyCDN adapter as a custom storage adapter in Laravel 9, install using the `v3` composer installer. ```bash @@ -78,6 +86,9 @@ Next, install the adapter to your `AppServiceProvider` to give Laravel's FileSys ), $config['pull_zone'] ); + + // for temporary URL support, define a signing key + $adapter->setTokenAuthKey('token-auth-signing-key'); return new FilesystemAdapter( new Filesystem($adapter, $config), @@ -98,6 +109,7 @@ Finally, add the `bunnycdn` driver into your `config/filesystems.php` configurat 'storage_zone' => env('BUNNYCDN_STORAGE_ZONE'), 'pull_zone' => env('BUNNYCDN_PULL_ZONE'), 'api_key' => env('BUNNYCDN_API_KEY'), + 'token_auth_key' => env('BUNNYCDN_TOKEN_AUTH_KEY', ''), // optional if you'd like signed URLs 'region' => env('BUNNYCDN_REGION', \PlatformCommunity\Flysystem\BunnyCDN\BunnyCDNRegion::DEFAULT) ], @@ -111,6 +123,7 @@ BUNNYCDN_STORAGE_ZONE=testing_storage_zone BUNNYCDN_PULL_ZONE=https://testing.b-cdn.net BUNNYCDN_API_KEY="api-key" # BUNNYCDN_REGION=uk +#BUNNYCDN_TOKEN_AUTH_KEY="your-token-auth-key" (optional, under CDN > Security > Token Authentication) ``` After that, you can use the `bunnycdn` disk in Laravel 9. From 5afba9d6bbb9892725f043dde3a2717aa39d431c Mon Sep 17 00:00:00 2001 From: Chris Page Date: Fri, 23 Jan 2026 14:23:19 +0000 Subject: [PATCH 3/7] Attempt at passing tests --- tests/FlysystemAdapterTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/FlysystemAdapterTest.php b/tests/FlysystemAdapterTest.php index 3de96b6..89116ab 100644 --- a/tests/FlysystemAdapterTest.php +++ b/tests/FlysystemAdapterTest.php @@ -392,6 +392,7 @@ public function test_checksum_throws_error_with_non_existing_file_on_default_alg public function test_checksum_throws_error_with_empty_checksum_from_client(): void { $client = $this->createMock(BunnyCDNClient::class); + $client->expects(self::exactly(1))->method('list')->willReturnCallback( function () { ['file' => $file, 'dir' => $dir] = Util::splitPathIntoDirectoryAndFile('file.txt'); From 8b08f20d13503cc2dda0253bd8ab9d3d8bc5cec7 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Jan 2026 14:50:59 +0000 Subject: [PATCH 4/7] feat: update PHPUnit version and add temporary URL generation tests --- composer.json | 2 +- tests/FlysystemAdapterTest.php | 16 +++++++++++++++- tests/PrefixTest.php | 13 ++++++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 9b4aa82..61bd124 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ "league/mime-type-detection": "^1.11" }, "require-dev": { - "phpunit/phpunit": "@stable", + "phpunit/phpunit": "^10.0", "league/flysystem-adapter-test-utilities": "^3", "league/flysystem-memory": "^3.0", "league/flysystem-path-prefixing": "^3.3", diff --git a/tests/FlysystemAdapterTest.php b/tests/FlysystemAdapterTest.php index 89116ab..4a4a3c6 100644 --- a/tests/FlysystemAdapterTest.php +++ b/tests/FlysystemAdapterTest.php @@ -91,7 +91,10 @@ private static function bunnyCDNClient(): BunnyCDNClient public static function createFilesystemAdapter(): FilesystemAdapter { - return new BunnyCDNAdapter(self::bunnyCDNClient(), static::$publicUrl); + $adapter = new BunnyCDNAdapter(self::bunnyCDNClient(), static::$publicUrl); + $adapter->setTokenAuthKey('test-token-auth-key'); + + return $adapter; } /** @@ -300,6 +303,17 @@ public function test_without_pullzone_url_error_thrown_accessing_url(): void $myAdapter->publicUrl('/path.txt', new Config()); } + /** + * @test + */ + public function generating_a_temporary_url(): void + { + if (! self::$isLive) { + $this->markTestSkipped('Temporary URL fetching requires a live BunnyCDN backend'); + } + parent::generating_a_temporary_url(); + } + /** * @test */ diff --git a/tests/PrefixTest.php b/tests/PrefixTest.php index 406555d..bc2a585 100644 --- a/tests/PrefixTest.php +++ b/tests/PrefixTest.php @@ -38,7 +38,10 @@ private static function bunnyCDNClient(): BunnyCDNClient private static function bunnyCDNAdapter(): BunnyCDNAdapter { - return new BunnyCDNAdapter(self::bunnyCDNClient(), 'https://example.org.local/assets/'); + $adapter = new BunnyCDNAdapter(self::bunnyCDNClient(), 'https://example.org.local/assets/'); + $adapter->setTokenAuthKey('test-token-auth-key'); + + return $adapter; } public static function createFilesystemAdapter(): FilesystemAdapter @@ -96,6 +99,14 @@ public function overwriting_a_file(): void }); } + /** + * @test + */ + public function generating_a_temporary_url(): void + { + $this->markTestSkipped('Temporary URL fetching requires a live BunnyCDN backend'); + } + /** * @test */ From 81489709bdc3d4d9f35274083d37537d190113b4 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Jan 2026 14:54:49 +0000 Subject: [PATCH 5/7] feat: update PHP versions in workflow configuration --- .github/workflows/php.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index e272c3f..cc91216 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -11,7 +11,7 @@ jobs: runs-on: 'ubuntu-latest' strategy: matrix: - php-versions: [ '8.0', '8.1', '8.2' ] + php-versions: [ '8.2', '8.3', '8.4', '8.5', ] steps: - uses: actions/checkout@v2 From edd59e153375c42603a7ccfbfd57faa054908c59 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Jan 2026 15:01:27 +0000 Subject: [PATCH 6/7] feat: enhance temporary URL generation tests for BunnyCDN adapter --- tests/FlysystemAdapterTest.php | 12 ++++++++---- tests/PrefixTest.php | 11 ++++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/tests/FlysystemAdapterTest.php b/tests/FlysystemAdapterTest.php index 4a4a3c6..c939913 100644 --- a/tests/FlysystemAdapterTest.php +++ b/tests/FlysystemAdapterTest.php @@ -308,10 +308,14 @@ public function test_without_pullzone_url_error_thrown_accessing_url(): void */ public function generating_a_temporary_url(): void { - if (! self::$isLive) { - $this->markTestSkipped('Temporary URL fetching requires a live BunnyCDN backend'); - } - parent::generating_a_temporary_url(); + $adapter = new BunnyCDNAdapter(self::bunnyCDNClient(), ''); + $adapter->setTokenAuthKey('test-key'); + + $expiresAt = new \DateTimeImmutable('+1 hour'); + $url = $adapter->temporaryUrl('path.txt', $expiresAt, new Config()); + + $this->assertStringContainsString('path.txt?token=', $url); + $this->assertStringContainsString('&expires=', $url); } /** diff --git a/tests/PrefixTest.php b/tests/PrefixTest.php index bc2a585..417a205 100644 --- a/tests/PrefixTest.php +++ b/tests/PrefixTest.php @@ -104,7 +104,16 @@ public function overwriting_a_file(): void */ public function generating_a_temporary_url(): void { - $this->markTestSkipped('Temporary URL fetching requires a live BunnyCDN backend'); + $adapter = new BunnyCDNAdapter(self::bunnyCDNClient(), 'https://example.org.local/assets/'); + $adapter->setTokenAuthKey('test-token-auth-key'); + + $prefixAdapter = new PathPrefixedAdapter($adapter, self::PREFIX_PATH); + + $expiresAt = new \DateTimeImmutable('+1 hour'); + $url = $prefixAdapter->temporaryUrl('path.txt', $expiresAt, new Config()); + + $this->assertStringContainsString(self::PREFIX_PATH.'/path.txt?token=', $url); + $this->assertStringContainsString('&expires=', $url); } /** From 55aa4238cc1331110515cdac9ecd74cf47ba0d8f Mon Sep 17 00:00:00 2001 From: Chris Page Date: Mon, 26 Jan 2026 14:02:53 +0000 Subject: [PATCH 7/7] Added pullzone URL to temporaryUrl generation (#93) --- src/BunnyCDNAdapter.php | 7 ++----- tests/TemporaryUrlTest.php | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/BunnyCDNAdapter.php b/src/BunnyCDNAdapter.php index a1c81b6..7301c35 100644 --- a/src/BunnyCDNAdapter.php +++ b/src/BunnyCDNAdapter.php @@ -582,14 +582,14 @@ public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config // extract elements from our path $parts = parse_url($path); - $path = $parts['path']; + $path = str_starts_with($parts['path'], '/') ? $path : '/'.$path; // extract our query params parse_str($parts['query'] ?? '', $params); ksort($params); // concatenate all of our data - return $path + return $this->pullzone_url.$path .(str_contains($path, '?') ? '&' : '?') .'token='.$this->buildSigningKey($path, $expiration, $params) .'&expires='.$expiration @@ -598,9 +598,6 @@ public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config private function buildSigningKey($path, int $expiration, array $params): string { - // prefix our path - $path = str_starts_with($path, '/') ? $path : '/'.$path; - // process our query params $query = implode('&', array_map(fn ($k, $v) => $k.'='.$v, array_keys($params), $params)); diff --git a/tests/TemporaryUrlTest.php b/tests/TemporaryUrlTest.php index 4c25cb0..6fe9065 100644 --- a/tests/TemporaryUrlTest.php +++ b/tests/TemporaryUrlTest.php @@ -25,13 +25,13 @@ public function test_temporary_url_throws_exception_if_not_configured() public function test_it_can_generate_signing_key() { $client = new BunnyCDNClient('test', 'test'); - $adapter = new BunnyCDNAdapter($client, 'pz-key'); + $adapter = new BunnyCDNAdapter($client, 'https://pz-url.co.uk'); $adapter->setTokenAuthKey('test-auth-key'); $expiresAt = new \DateTimeImmutable('+1 hour'); $url = $adapter->temporaryUrl('testing.txt', $expiresAt, new Config()); - $this->assertStringContainsString('testing.txt?token=', $url); + $this->assertStringContainsString('https://pz-url.co.uk/testing.txt?token=', $url); $this->assertStringContainsString('expires='.$expiresAt->getTimestamp(), $url); } }