From 880701c9fb28fce61877e8cce60e309400a9d878 Mon Sep 17 00:00:00 2001 From: edalzell Date: Sun, 6 Sep 2026 13:13:53 -0700 Subject: [PATCH 1/4] fix: ignore empty or invalid timezone tag params Empty timezone values from unresolved dictionary fields were passed straight into Carbon and crashed occurrence output. --- src/Events.php | 27 +++++++++++++++++++++++---- src/Tags/Events.php | 2 +- tests/Tags/EventsTest.php | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/Events.php b/src/Events.php index 5e19268..9c13f46 100644 --- a/src/Events.php +++ b/src/Events.php @@ -4,6 +4,7 @@ use Carbon\CarbonImmutable; use Carbon\CarbonInterface; +use DateTimeZone; use Exception; use Illuminate\Pagination\Paginator; use Illuminate\Support\Collection; @@ -43,7 +44,10 @@ class Events public static function defaultTimezone(): string { - return static::setting('timezone'); + return static::resolveTimezone(static::setting('timezone')) + ?? static::resolveTimezone(config('statamic.system.display_timezone')) + ?? static::resolveTimezone(config('app.timezone')) + ?? 'UTC'; } public static function fromCollection(string $handle): self @@ -56,6 +60,21 @@ public static function fromEntry(string $id): self return new static(new Parameters(['event' => $id])); } + public static function resolveTimezone(mixed $timezone): ?string + { + if (! is_string($timezone) || ! filled($timezone)) { + return null; + } + + try { + new DateTimeZone($timezone); + } catch (Exception) { + return null; + } + + return $timezone; + } + public function __construct(Parameters $params) { throw_if( @@ -71,7 +90,7 @@ public function __construct(Parameters $params) ->offset(offset: $params->int('offset')) ->pagination(page: Paginator::resolveCurrentPage(), perPage: $params->int('paginate')) ->sort($params->get('sort', 'asc')) - ->timezone(timezone: $params->get('timezone', static::defaultTimezone())); + ->timezone(timezone: $params->get('timezone')); } public static function setting(string $key, $default = null): mixed @@ -172,9 +191,9 @@ public function terms(string|array $terms): self return $this; } - public function timezone(string $timezone): self + public function timezone(?string $timezone = null): self { - $this->timezone = $timezone; + $this->timezone = static::resolveTimezone($timezone) ?? static::defaultTimezone(); return $this; } diff --git a/src/Tags/Events.php b/src/Tags/Events.php index 2f8fb2c..e89fdf6 100755 --- a/src/Tags/Events.php +++ b/src/Tags/Events.php @@ -38,7 +38,7 @@ public function calendar(): Collection $month = $this->params->get('month', now()->englishMonth); $year = $this->params->get('year', now()->year); - $timezone = $this->params->get('timezone', Generator::defaultTimezone()); + $timezone = Generator::resolveTimezone($this->params->get('timezone')) ?? Generator::defaultTimezone(); $from = parse_date($month.' '.$year)->shiftTimezone($timezone)->startOfMonth()->startOfWeek(); $to = parse_date($month.' '.$year)->shiftTimezone($timezone)->endOfMonth()->endOfWeek(); diff --git a/tests/Tags/EventsTest.php b/tests/Tags/EventsTest.php index 414d5d6..f897772 100755 --- a/tests/Tags/EventsTest.php +++ b/tests/Tags/EventsTest.php @@ -8,6 +8,7 @@ use Statamic\Facades\Site as SiteFacade; use Statamic\Sites\Site; use Statamic\Support\Arr; +use TransformStudios\Events\Events; use TransformStudios\Events\Tags\Events as EventsTag; beforeEach(function () { @@ -663,6 +664,42 @@ ->first()->start->timezone->getName()->toBe('America/Vancouver'); }); +it('falls back to the default timezone when the timezone param is empty', function () { + Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); + + $this->tag + ->setContext([]) + ->setParameters([ + 'collection' => 'events', + 'from' => Carbon::now()->subDay(), + 'timezone' => '', + 'to' => Carbon::now()->addDays(2), + ]); + + $occurrences = $this->tag->between(); + + expect($occurrences)->toHaveCount(1) + ->first()->start->timezone->getName()->toBe(Events::defaultTimezone()); +}); + +it('falls back to the default timezone when the timezone param is invalid', function () { + Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); + + $this->tag + ->setContext([]) + ->setParameters([ + 'collection' => 'events', + 'from' => Carbon::now()->subDay(), + 'timezone' => 'not-a-real-timezone', + 'to' => Carbon::now()->addDays(2), + ]); + + $occurrences = $this->tag->between(); + + expect($occurrences)->toHaveCount(1) + ->first()->start->timezone->getName()->toBe(Events::defaultTimezone()); +}); + it('sets "spanning"', function () { Carbon::setTestNow(now()->setTimeFromTimeString('10:00')); From 15b97b4562bc8961dde60b1e57dd8fd77feeb8bd Mon Sep 17 00:00:00 2001 From: edalzell Date: Sun, 6 Sep 2026 13:24:36 -0700 Subject: [PATCH 2/4] refactor: always resolve to a valid timezone Collapse validation and fallback chain into resolveTimezone() so callers never handle null. --- src/Events.php | 38 +++++++++++++++++++++----------------- src/Tags/Events.php | 2 +- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/Events.php b/src/Events.php index 9c13f46..04f763a 100644 --- a/src/Events.php +++ b/src/Events.php @@ -44,10 +44,7 @@ class Events public static function defaultTimezone(): string { - return static::resolveTimezone(static::setting('timezone')) - ?? static::resolveTimezone(config('statamic.system.display_timezone')) - ?? static::resolveTimezone(config('app.timezone')) - ?? 'UTC'; + return static::resolveTimezone(); } public static function fromCollection(string $handle): self @@ -60,19 +57,15 @@ public static function fromEntry(string $id): self return new static(new Parameters(['event' => $id])); } - public static function resolveTimezone(mixed $timezone): ?string + public static function resolveTimezone(mixed $timezone = null): string { - if (! is_string($timezone) || ! filled($timezone)) { - return null; - } - - try { - new DateTimeZone($timezone); - } catch (Exception) { - return null; - } - - return $timezone; + return collect([ + $timezone, + static::setting('timezone'), + config('statamic.system.display_timezone'), + config('app.timezone'), + 'UTC', + ])->first(fn (mixed $candidate) => is_string($candidate) && filled($candidate) && static::isValidTimezone($candidate)) ?? 'UTC'; } public function __construct(Parameters $params) @@ -193,7 +186,7 @@ public function terms(string|array $terms): self public function timezone(?string $timezone = null): self { - $this->timezone = static::resolveTimezone($timezone) ?? static::defaultTimezone(); + $this->timezone = static::resolveTimezone($timezone); return $this; } @@ -214,6 +207,17 @@ public function upcoming(int $limit = 1): EntryCollection|LengthAwarePaginator ); } + private static function isValidTimezone(string $timezone): bool + { + try { + new DateTimeZone($timezone); + } catch (Exception) { + return false; + } + + return true; + } + private function output(callable $type, string|CarbonInterface $from): EntryCollection|LengthAwarePaginator { $occurrences = $this->entries()->occurrences(generator: $type, from: $from); diff --git a/src/Tags/Events.php b/src/Tags/Events.php index e89fdf6..09c0b0f 100755 --- a/src/Tags/Events.php +++ b/src/Tags/Events.php @@ -38,7 +38,7 @@ public function calendar(): Collection $month = $this->params->get('month', now()->englishMonth); $year = $this->params->get('year', now()->year); - $timezone = Generator::resolveTimezone($this->params->get('timezone')) ?? Generator::defaultTimezone(); + $timezone = Generator::resolveTimezone($this->params->get('timezone')); $from = parse_date($month.' '.$year)->shiftTimezone($timezone)->startOfMonth()->startOfWeek(); $to = parse_date($month.' '.$year)->shiftTimezone($timezone)->endOfMonth()->endOfWeek(); From 43b3f8732b8f1fdc57f73b575c022165e1618ba3 Mon Sep 17 00:00:00 2001 From: edalzell Date: Sun, 6 Sep 2026 13:28:27 -0700 Subject: [PATCH 3/4] refactor: move timezone candidate checks into isValidTimezone --- src/Events.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Events.php b/src/Events.php index 04f763a..caba357 100644 --- a/src/Events.php +++ b/src/Events.php @@ -65,7 +65,7 @@ public static function resolveTimezone(mixed $timezone = null): string config('statamic.system.display_timezone'), config('app.timezone'), 'UTC', - ])->first(fn (mixed $candidate) => is_string($candidate) && filled($candidate) && static::isValidTimezone($candidate)) ?? 'UTC'; + ])->first(fn (mixed $candidate) => static::isValidTimezone($candidate)) ?? 'UTC'; } public function __construct(Parameters $params) @@ -207,8 +207,12 @@ public function upcoming(int $limit = 1): EntryCollection|LengthAwarePaginator ); } - private static function isValidTimezone(string $timezone): bool + private static function isValidTimezone(mixed $timezone): bool { + if (! is_string($timezone) || ! filled($timezone)) { + return false; + } + try { new DateTimeZone($timezone); } catch (Exception) { From a46091ed86e739b99eb44a4420b417724f3bf3e1 Mon Sep 17 00:00:00 2001 From: edalzell Date: Sun, 6 Sep 2026 13:30:27 -0700 Subject: [PATCH 4/4] refactor: drop redundant UTC fallback after collect first --- src/Events.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Events.php b/src/Events.php index caba357..352a48b 100644 --- a/src/Events.php +++ b/src/Events.php @@ -65,7 +65,7 @@ public static function resolveTimezone(mixed $timezone = null): string config('statamic.system.display_timezone'), config('app.timezone'), 'UTC', - ])->first(fn (mixed $candidate) => static::isValidTimezone($candidate)) ?? 'UTC'; + ])->first(fn (mixed $candidate) => static::isValidTimezone($candidate)); } public function __construct(Parameters $params)