Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/Http/Controllers/Posts/PostController.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@
use App\Services\LdJsonService;
use App\Services\PostService;
use App\Traits\PostTrait;
use App\Traits\SessionTimezoneTrait;
use App\Traits\SubsiteTrait;
use Illuminate\Contracts\View\View;

final class PostController extends BaseController
{
use PostTrait;
use SessionTimezoneTrait;
use SubsiteTrait;

protected int $subsiteId;
Expand Down Expand Up @@ -55,6 +57,7 @@ public function show(Post $post): View
'next' => $post->next(),
'previous' => $post->previous(),
'canonicalUrl' => $this->getCanonicalUrl($post),
'displayTimezone' => $this->getDisplayTimezone(),
'relatedPosts' => $relatedPosts,
'subdomain' => $subdomain,
'useLivewire' => true,
Expand Down
2 changes: 2 additions & 0 deletions app/Livewire/Comments/CommentComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use App\Models\Post;
use App\Models\User;
use App\Traits\CommentComponentTrait;
use App\Traits\SessionTimezoneTrait;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\On;
Expand All @@ -18,6 +19,7 @@
final class CommentComponent extends Component
{
use CommentComponentTrait;
use SessionTimezoneTrait;

// Data
public ?int $authorizedUserId;
Expand Down
19 changes: 19 additions & 0 deletions app/Livewire/Localization/SwitchTimezoneComponent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace App\Livewire\Localization;

use App\Traits\SessionTimezoneTrait;
use Illuminate\Contracts\View\View;
use Livewire\Component;

final class SwitchTimezoneComponent extends Component
{
use SessionTimezoneTrait;

public function render(): View
{
return view('livewire.localization.switch-timezone-component');
}
}
5 changes: 4 additions & 1 deletion app/Livewire/Posts/PostIndexComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Models\Post;
use App\Traits\PaginationTrait;
use App\Traits\PostTrait;
use App\Traits\SessionTimezoneTrait;
use App\Traits\SubsiteTrait;
use Illuminate\Contracts\Pagination\CursorPaginator;
use Illuminate\Contracts\View\View;
Expand All @@ -19,6 +20,7 @@ final class PostIndexComponent extends Component
{
use PaginationTrait;
use PostTrait;
use SessionTimezoneTrait;
use SubsiteTrait;
use WithPagination;

Expand Down Expand Up @@ -59,6 +61,7 @@ public function render(): View

return view('livewire.posts.post-index-component', [
'posts' => $posts,
'displayTimezone' => $this->getDisplayTimezone(),
]);
}

Expand All @@ -72,7 +75,7 @@ public function query(): Builder
private function getPosts(): CursorPaginator
{
$dateQueries = [
DB::raw('DATE_FORMAT(posts.created_at, "%m-%d") as month_day'),
DB::raw("DATE_FORMAT(CONVERT_TZ(posts.created_at, 'UTC', " . DB::connection()->getPdo()->quote($this->getDisplayTimezone()) . "), '%m-%d') as month_day"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess there is some input validation on this, but using this raw instead of parameterized feels like a potential SQL injection vector

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm expecting DB::connection()->getPdo()->quote to do the appropriate escaping to avoid SQL injection. It's unfortunately quite awkward to add bind parameters to queries constructed by the query builder as it only uses ? placeholders, not named ones, so then you have to figure out the right ordering of bind parameters in a way that respects the rest of the query as constructed by Eloquent. I spent a while trying to do that and gave up, hence this attempt to manage interpolating a properly escaped value 😓

DB::raw('COUNT(*) as total_posts'),
];

Expand Down
39 changes: 39 additions & 0 deletions app/Traits/SessionTimezoneTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace App\Traits;

use Carbon\CarbonTimezone;
use Carbon\Exceptions\InvalidTimeZoneException;
use Livewire\Attributes\Locked;
use Livewire\Attributes\Session;

trait SessionTimezoneTrait
{
use LoggingTrait;

#[Session(key: 'displayTimezone')]
#[Locked]
public ?string $displayTimezone = null;

public function getDisplayTimezone(): string
{
// For some reason, calling session('displayTimezone') will sometimes return a value
// when $this->displayTimezone is null.
return $this->displayTimezone ?? session('displayTimezone') ?? config('app.timezone');
}

public function setDisplayTimezone(string $timezone): void
{
try {
// Try to create a CarbonTimezone object to validate the timezone string.
CarbonTimezone::create($timezone);

// If we get here, the timezone is valid, so we can set it.
$this->displayTimezone = $timezone;
} catch (InvalidTimeZoneException $e) {
$this->logError('Invalid timezone: ' . $timezone);
}
}
}
6 changes: 5 additions & 1 deletion app/View/Components/Dates/FormattedDateTimeComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@

namespace App\View\Components\Dates;

use App\Traits\SessionTimezoneTrait;
use Carbon\Carbon;
use Illuminate\Contracts\View\View;
use Illuminate\View\Component;

final class FormattedDateTimeComponent extends Component
{
use SessionTimezoneTrait;

public Carbon $date;
public string $format = 'Y-m-d H:i:s';

Expand All @@ -36,7 +39,8 @@ public function render(): View

private function getFormattedDate(): string
{
$formattedDate = $this->date->format($this->format());
$date = $this->date->tz($this->getDisplayTimezone());
$formattedDate = $date->format($this->format());

return $this->addPeriods($formattedDate);
}
Expand Down
4 changes: 2 additions & 2 deletions resources/views/comments/partials/comment-timestamp.blade.php
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<time datetime="{{ $comment->created_at->format('Y-m-d H:i:d') }}">
<a href="#{{ $comment->id }}" class="footer-info" title="{{ trans('Permanent link to this comment') }}">
<x-icons.icon-component filename="clock" class="icon-small" />
{{ $comment->created_at->format('g:i a') }}
{{ $comment->created_at->tz($displayTimezone)->format('g:i a') }}
</a>

<span class="footer-info">
<x-icons.icon-component filename="calendar3" class="icon-small" />
{{ $comment->created_at->format('F j') }}
{{ $comment->created_at->tz($displayTimezone)->format('F j') }}
</span>
</time>
2 changes: 2 additions & 0 deletions resources/views/layouts/app.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
<meta name="ckeditor-config" content='@json(config("ckeditor"))'>
@endif

<livewire:localization.switch-timezone-component />

</head>
<body>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div style="display: none;" wire:init="$js.setDisplayTimezone"></div>

@script
<script>
$js('setDisplayTimezone', () => {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any concerns with potential IANA zone mismatches between what a browser will supply and what PHP understands? there's some MDN discussion about non-canonicalization of TZ names,

also, from an infra perspective, this will probably require keeping TZDB up-to-date on the relevant App & SQL instances. Probably not a real problem, but just calling out to avoid missing these updates then getting unknown timezones.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be ok as long as we're tolerant of unknown timezones server-side and let people override from a server-supplied list in preferences. For the majority of people just browsing casually the idea was to not have times show up in a weird timezone.

$wire.setDisplayTimezone(timezone);
});
</script>
@endscript
4 changes: 2 additions & 2 deletions resources/views/posts/show.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@
<p class="dateline">
<time datetime="{{ $post->created_at->format('Y-m-d H:i:d') }}">
<x-icons.icon-component filename="calendar3" class="icon-small" />
{{ $post->created_at->format('F j, Y') }}
{{ $post->created_at->tz($displayTimezone)->format('F j, Y') }}

<x-icons.icon-component filename="clock" class="icon-small" />
{{ $post->created_at->format('g:i a') }}
{{ $post->created_at->tz($displayTimezone)->format('g:i a') }}
</time>

<x-posts.post-rss-button :post="$post" />
Expand Down