diff --git a/StarResponseController.php b/StarResponseController.php index af989f2..5df06ed 100644 --- a/StarResponseController.php +++ b/StarResponseController.php @@ -218,8 +218,14 @@ public static function reset(): void */ private static function sendPublicCacheHeaders(): void { - $maxAge = (int) apply_filters('starcache_max_age', self::DEFAULT_MAX_AGE); - $swr = (int) apply_filters('starcache_stale_while_revalidate', self::DEFAULT_STALE_WHILE_REVALIDATE); + $maxAge = self::sanitizeDirectiveSeconds( + apply_filters('starcache_max_age', self::DEFAULT_MAX_AGE), + self::DEFAULT_MAX_AGE + ); + $swr = self::sanitizeDirectiveSeconds( + apply_filters('starcache_stale_while_revalidate', self::DEFAULT_STALE_WHILE_REVALIDATE), + self::DEFAULT_STALE_WHILE_REVALIDATE + ); header('Cache-Control: public, max-age=' . $maxAge . ', stale-while-revalidate=' . $swr); @@ -231,6 +237,30 @@ private static function sendPublicCacheHeaders(): void header('X-StarCache-Context: ' . StarCacheContext::hash()); } + /** + * Normalize cache directive durations to a safe, non-negative integer. + * + * Negative or non-sensical values from third-party filters can result in + * invalid Cache-Control headers. This guard keeps header output valid and + * predictable while still allowing plugin-level TTL customization. + * + * @param mixed $value Requested TTL value from filter callbacks. + * @param int $fallback Default value used when the request is invalid. + */ + private static function sanitizeDirectiveSeconds(mixed $value, int $fallback): int + { + if (!is_numeric($value)) { + return max(0, $fallback); + } + + $seconds = (int) $value; + if ($seconds < 0) { + return max(0, $fallback); + } + + return $seconds; + } + /** * Emit no-cache headers. */ diff --git a/tests/UnitTests.php b/tests/UnitTests.php index dfe0e81..c10d79c 100644 --- a/tests/UnitTests.php +++ b/tests/UnitTests.php @@ -484,6 +484,28 @@ public function testResponseControllerNotEligibleWhenAuthenticated(): void $this->assertFalse(StarResponseController::isEligible()); } + public function testResponseControllerSanitizeDirectiveSecondsFallsBackForNegativeValue(): void + { + $method = new \ReflectionMethod(StarResponseController::class, 'sanitizeDirectiveSeconds'); + $method->setAccessible(true); + + $maxAge = $method->invoke(null, -15, StarResponseController::DEFAULT_MAX_AGE); + $swr = $method->invoke(null, -3, StarResponseController::DEFAULT_STALE_WHILE_REVALIDATE); + + $this->assertSame(StarResponseController::DEFAULT_MAX_AGE, $maxAge); + $this->assertSame(StarResponseController::DEFAULT_STALE_WHILE_REVALIDATE, $swr); + } + + public function testResponseControllerSanitizeDirectiveSecondsFallsBackForInvalidType(): void + { + $method = new \ReflectionMethod(StarResponseController::class, 'sanitizeDirectiveSeconds'); + $method->setAccessible(true); + + $this->assertSame(120, $method->invoke(null, null, 120)); + $this->assertSame(90, $method->invoke(null, 'not-a-number', 90)); + $this->assertSame(0, $method->invoke(null, false, -10)); + } + // ========================================================================= // StarVersionStore — renamed groups // =========================================================================