diff --git a/lib/Client.php b/lib/Client.php index 020579f..4f41443 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -103,14 +103,18 @@ public function __construct( ?string $personalAPIKey = null, bool $loadFeatureFlags = true, ) { - $this->apiKey = $apiKey; - $this->personalAPIKey = $personalAPIKey; + $this->apiKey = trim($apiKey); + $this->personalAPIKey = StringNormalizer::normalizeOptional($personalAPIKey); $this->options = $options; $this->debug = $options["debug"] ?? false; + $this->options['host'] = StringNormalizer::normalizeHost($options['host'] ?? null); + if ($this->apiKey === '') { + error_log('[PostHog][Client] apiKey is empty after trimming whitespace; check your project API key'); + } $Consumer = self::CONSUMERS[$options["consumer"] ?? "lib_curl"]; - $this->consumer = new $Consumer($apiKey, $options, $httpClient); + $this->consumer = new $Consumer($this->apiKey, $this->options, $httpClient); $this->httpClient = $httpClient !== null ? $httpClient : new HttpClient( - $options['host'] ?? "app.posthog.com", + $this->options['host'], $options['ssl'] ?? true, (int) ($options['maximum_backoff_duration'] ?? 10000), false, diff --git a/lib/PostHog.php b/lib/PostHog.php index e207eba..7cf5026 100644 --- a/lib/PostHog.php +++ b/lib/PostHog.php @@ -42,9 +42,10 @@ public static function init( // Infer ssl from the host protocol if the user hasn't explicitly set it if ($rawHost !== null && !array_key_exists("ssl", $options)) { - if (str_starts_with($rawHost, "http://")) { + $normalizedHost = StringNormalizer::normalizeHost($rawHost); + if (str_starts_with($normalizedHost, "http://")) { $options["ssl"] = false; - } elseif (str_starts_with($rawHost, "https://")) { + } elseif (str_starts_with($normalizedHost, "https://")) { $options["ssl"] = true; } } @@ -373,9 +374,8 @@ public static function getClient(): Client private static function cleanHost(?string $host): string { - if (!isset($host)) { - return $host; - } + $host = StringNormalizer::normalizeHost($host); + // remove protocol if (substr($host, 0, 8) === "https://") { $host = str_replace('https://', '', $host); diff --git a/lib/QueueConsumer.php b/lib/QueueConsumer.php index 0eaec96..1f22ce3 100644 --- a/lib/QueueConsumer.php +++ b/lib/QueueConsumer.php @@ -13,7 +13,7 @@ abstract class QueueConsumer extends Consumer protected $max_queue_size = 1000; protected $batch_size = 100; protected $maximum_backoff_duration = 10000; // Set maximum waiting limit to 10s - protected $host = "app.posthog.com"; + protected $host = "us.i.posthog.com"; protected $compress_request = false; /** diff --git a/lib/StringNormalizer.php b/lib/StringNormalizer.php new file mode 100644 index 0000000..34c8ac7 --- /dev/null +++ b/lib/StringNormalizer.php @@ -0,0 +1,23 @@ +setUp(MockedResponses::FLAGS_V2_RESPONSE, personalApiKey: " \n\t "); + $this->assertTrue(PostHog::isFeatureEnabled('simple-test', 'user-id')); + $this->assertEquals( + [ + [ + "path" => "/flags/?v=2", + "payload" => sprintf('{"api_key":"%s","distinct_id":"user-id","person_properties":{"distinct_id":"user-id"}}', self::FAKE_API_KEY), + "extraHeaders" => [0 => 'User-Agent: posthog-php/' . PostHog::VERSION], + "requestOptions" => ["timeout" => 3000, "shouldRetry" => false], + ], + ], + $this->http_client->calls + ); + } + /** * @dataProvider decideResponseCases */ diff --git a/test/PostHogTest.php b/test/PostHogTest.php index 98f6379..44d5ce8 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -63,6 +63,39 @@ public function testInitWithEnvApiKey(): void putenv(PostHog::ENV_API_KEY); } + public function testClientTrimsWhitespaceSensitiveConfig(): void + { + $client = new Client( + " \nrandom_key\t ", + ["host" => " \nhttps://app.posthog.com/\t ", "debug" => true], + $this->http_client, + " \n\t " + ); + + $ref = new \ReflectionClass($client); + $apiKeyProp = $ref->getProperty('apiKey'); + $apiKeyProp->setAccessible(true); + $personalApiKeyProp = $ref->getProperty('personalAPIKey'); + $personalApiKeyProp->setAccessible(true); + $optionsProp = $ref->getProperty('options'); + $optionsProp->setAccessible(true); + + $this->assertEquals('random_key', $apiKeyProp->getValue($client)); + $this->assertNull($personalApiKeyProp->getValue($client)); + $this->assertEquals('https://app.posthog.com/', $optionsProp->getValue($client)['host']); + } + + public function testClientLogsWhenApiKeyIsEmptyAfterTrimmingWhitespace(): void + { + new Client(" \n\t ", ["debug" => true], $this->http_client); + + global $errorMessages; + $this->assertContains( + '[PostHog][Client] apiKey is empty after trimming whitespace; check your project API key', + $errorMessages + ); + } + public function testInitWithHttpHostSetsSslFalse(): void { PostHog::init("random_key", ["host" => "http://localhost:8010"]); @@ -85,6 +118,45 @@ public function testInitWithHttpHostSetsSslFalse(): void $this->assertFalse($sslProp->getValue($httpClient), 'HttpClient should use ssl=false for http:// hosts'); } + public function testInitDefaultsBlankHostAfterTrimmingWhitespace(): void + { + PostHog::init("random_key", ["host" => " \n\t "]); + + $client = PostHog::getClient(); + $ref = new \ReflectionClass($client); + $consumerProp = $ref->getProperty('consumer'); + $consumerProp->setAccessible(true); + $consumer = $consumerProp->getValue($client); + + $cRef = new \ReflectionClass($consumer); + $hostProp = $cRef->getProperty('host'); + $hostProp->setAccessible(true); + + $this->assertEquals('us.i.posthog.com', $hostProp->getValue($consumer)); + } + + public function testInitWithWhitespacePaddedHttpHostSetsSslFalse(): void + { + PostHog::init("random_key", ["host" => " \nhttp://localhost:8010\t "]); + + $client = PostHog::getClient(); + $ref = new \ReflectionClass($client); + $consumerProp = $ref->getProperty('consumer'); + $consumerProp->setAccessible(true); + $consumer = $consumerProp->getValue($client); + + $cRef = new \ReflectionClass($consumer); + $httpProp = $cRef->getProperty('httpClient'); + $httpProp->setAccessible(true); + $httpClient = $httpProp->getValue($consumer); + + $hRef = new \ReflectionClass($httpClient); + $sslProp = $hRef->getProperty('useSsl'); + $sslProp->setAccessible(true); + + $this->assertFalse($sslProp->getValue($httpClient), 'HttpClient should use ssl=false for whitespace-padded http:// hosts'); + } + public function testInitWithHttpsHostSetsSslTrue(): void { PostHog::init("random_key", ["host" => "https://app.posthog.com"]);