From 5404040447a3268771e2cb3141e303888a9b5694 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Mon, 26 Oct 2026 15:32:42 +0000 Subject: [PATCH] send a `Statamic/{version}` user agent with outpost and marketplace requests --- src/Jobs/ReportThemeUsage.php | 3 ++- src/Licensing/Outpost.php | 5 ++++- src/Marketplace/Client.php | 2 ++ src/StarterKits/LicenseManager.php | 7 ++++--- tests/Jobs/ReportThemeUsageTest.php | 16 ++++++++++++++++ tests/Licensing/OutpostTest.php | 14 ++++++++++++++ tests/Marketplace/ClientTest.php | 23 +++++++++++++++++++++++ tests/StarterKits/InstallTest.php | 26 ++++++++++++++++++++++++++ 8 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 tests/Marketplace/ClientTest.php diff --git a/src/Jobs/ReportThemeUsage.php b/src/Jobs/ReportThemeUsage.php index f74de74ebf7..9e421a1ef33 100644 --- a/src/Jobs/ReportThemeUsage.php +++ b/src/Jobs/ReportThemeUsage.php @@ -8,6 +8,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Facades\Http; use Statamic\Licensing\Outpost; +use Statamic\Statamic; class ReportThemeUsage implements ShouldQueue { @@ -34,7 +35,7 @@ public function handle(Outpost $outpost): void return; } - Http::post('https://outpost.statamic.com/v3/theme', ['theme' => $newThemeId]); + Http::withUserAgent('Statamic/'.Statamic::version())->post('https://outpost.statamic.com/v3/theme', ['theme' => $newThemeId]); } private function themeId($theme) diff --git a/src/Licensing/Outpost.php b/src/Licensing/Outpost.php index 2f1a01fd61a..36d72a6a668 100644 --- a/src/Licensing/Outpost.php +++ b/src/Licensing/Outpost.php @@ -85,7 +85,10 @@ private function performRequest() } $response = $this->client->request('POST', self::ENDPOINT, [ - 'headers' => ['accept' => 'application/json'], + 'headers' => [ + 'accept' => 'application/json', + 'user-agent' => 'Statamic/'.Statamic::version(), + ], 'json' => $this->payload(), 'timeout' => self::REQUEST_TIMEOUT, ]); diff --git a/src/Marketplace/Client.php b/src/Marketplace/Client.php index 155623c3761..361157ed598 100644 --- a/src/Marketplace/Client.php +++ b/src/Marketplace/Client.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Cache; +use Statamic\Statamic; class Client { @@ -72,6 +73,7 @@ private function request(string $method, string $endpoint, array $params = []) $response = Guzzle::request($method, $endpoint, [ 'verify' => $this->verifySsl, 'timeout' => 5, + 'headers' => ['user-agent' => 'Statamic/'.Statamic::version()], ($method === 'GET' ? 'query' : 'json') => $params, ]); diff --git a/src/StarterKits/LicenseManager.php b/src/StarterKits/LicenseManager.php index 9e1fa4bc04c..2a1554914d4 100644 --- a/src/StarterKits/LicenseManager.php +++ b/src/StarterKits/LicenseManager.php @@ -5,6 +5,7 @@ use Illuminate\Console\Command; use Illuminate\Support\Facades\Http; use Statamic\Console\NullConsole; +use Statamic\Statamic; use function Laravel\Prompts\text; @@ -51,7 +52,7 @@ public function isValid(): bool */ public function completeInstall(): void { - Http::post(self::OUTPOST_ENDPOINT.'installed', [ + Http::withUserAgent('Statamic/'.Statamic::version())->post(self::OUTPOST_ENDPOINT.'installed', [ 'license' => $this->licenseKey, 'configured_site_license' => config('statamic.system.license_key'), 'package' => $this->package, @@ -107,7 +108,7 @@ private function performValidation(): self */ private function outpostGetStarterKitDetails(): self { - $response = Http::get(self::OUTPOST_ENDPOINT.$this->package); + $response = Http::withUserAgent('Statamic/'.Statamic::version())->get(self::OUTPOST_ENDPOINT.$this->package); if ($response->status() !== 200) { return false; @@ -139,7 +140,7 @@ private function outpostValidatesLicense(): bool return false; } - $response = Http::post(self::OUTPOST_ENDPOINT.'validate', [ + $response = Http::withUserAgent('Statamic/'.Statamic::version())->post(self::OUTPOST_ENDPOINT.'validate', [ 'license' => $this->licenseKey, 'configured_site_license' => config('statamic.system.license_key'), 'package' => $this->package, diff --git a/tests/Jobs/ReportThemeUsageTest.php b/tests/Jobs/ReportThemeUsageTest.php index 16330079f57..3281bb1d4db 100644 --- a/tests/Jobs/ReportThemeUsageTest.php +++ b/tests/Jobs/ReportThemeUsageTest.php @@ -60,6 +60,22 @@ public static function reportProvider() ]; } + #[Test] + public function it_sends_the_statamic_user_agent() + { + Http::preventStrayRequests(); + Http::fake(); + + $outpost = Mockery::mock(Outpost::class); + $outpost->shouldReceive('usingLicenseKeyFile')->andReturnFalse(); + + $job = new ReportThemeUsage(null, ['id' => 1]); + + $job->handle($outpost); + + Http::assertSent(fn (Request $request) => $request->header('User-Agent') === ['Statamic/3.0.0-testing']); + } + #[Test] public function it_doesnt_report_if_using_license_key_file() { diff --git a/tests/Licensing/OutpostTest.php b/tests/Licensing/OutpostTest.php index 836b0007354..cde764230ce 100644 --- a/tests/Licensing/OutpostTest.php +++ b/tests/Licensing/OutpostTest.php @@ -6,6 +6,8 @@ use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; use Illuminate\Encryption\Encrypter; @@ -75,6 +77,18 @@ public function it_contacts_the_outpost_and_caches_the_response_for_50_to_59_min $this->assertCachedUntilExpiry($response); } + #[Test] + public function it_sends_the_statamic_user_agent() + { + $history = []; + $handler = HandlerStack::create(new MockHandler([new Response(200, [], '{}')])); + $handler->push(Middleware::history($history)); + + $this->outpost(new Client(['handler' => $handler]))->response(); + + $this->assertEquals('Statamic/3.0.0-testing', $history[0]['request']->getHeaderLine('User-Agent')); + } + #[Test] public function the_cached_response_is_used() { diff --git a/tests/Marketplace/ClientTest.php b/tests/Marketplace/ClientTest.php new file mode 100644 index 00000000000..d9189711f86 --- /dev/null +++ b/tests/Marketplace/ClientTest.php @@ -0,0 +1,23 @@ +once() + ->withArgs(fn ($method, $endpoint, $options) => $options['headers']['user-agent'] === 'Statamic/3.0.0-testing') + ->andReturn(new Response(200, [], '{}')); + + (new Client)->get('packages'); + } +} diff --git a/tests/StarterKits/InstallTest.php b/tests/StarterKits/InstallTest.php index fff7b9bbb71..0c4b7a11dde 100644 --- a/tests/StarterKits/InstallTest.php +++ b/tests/StarterKits/InstallTest.php @@ -672,6 +672,32 @@ public function it_installs_paid_starter_kit_with_valid_license_key() $this->assertFileExists(base_path('copied.md')); } + #[Test] + public function it_sends_the_statamic_user_agent_to_the_outpost() + { + Config::set('statamic.system.license_key', 'site-key'); + + $this->installCoolRunnings([], [ + 'outpost.*/v3/starter-kits/statamic/cool-runnings' => Http::response(['data' => [ + 'price' => 100, + 'slug' => 'cool-runnings', + 'seller' => ['slug' => 'statamic'], + ]], 200), + 'outpost.*/v3/starter-kits/validate' => Http::response(['data' => [ + 'valid' => true, + ]], 200), + '*' => Http::response('', 200), + ]); + + $outpostRequests = Http::recorded(fn ($request) => str_contains($request->url(), 'outpost.statamic.com')); + + $this->assertCount(3, $outpostRequests); + + $outpostRequests->each(function ($pair) { + $this->assertEquals(['Statamic/3.0.0-testing'], $pair[0]->header('User-Agent')); + }); + } + #[Test] public function it_doesnt_install_paid_starter_kit_with_invalid_license_key() {