- add rotating admin refresh tokens and password gates\n- expand ticket galleries, exports, and client resources\n- improve IP allocation auditing and dashboard data\n- enforce encrypted client access tokens on public routes
134 lines
3.6 KiB
PHP
134 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Models\Ticket;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\URL;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
final class TicketGallery
|
|
{
|
|
private const DISK = 'local';
|
|
|
|
private const LEGACY_DISK = 'public';
|
|
|
|
private const LEGACY_URL_SEGMENT = '/storage/';
|
|
|
|
/**
|
|
* @param UploadedFile[] $files
|
|
* @return string[]
|
|
*/
|
|
public static function store(array $files, int $customerId): array
|
|
{
|
|
return array_map(
|
|
fn (UploadedFile $file) => $file->store("ticket-attachments/{$customerId}", self::DISK),
|
|
$files,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public static function clientUrls(Ticket $ticket): array
|
|
{
|
|
return array_map(
|
|
fn (int $index) => "/v1/client/tickets/{$ticket->ticket_num}/images/{$index}",
|
|
array_keys(self::entries($ticket)),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
public static function adminUrls(Ticket $ticket): array
|
|
{
|
|
return array_map(
|
|
fn (int $index) => URL::temporarySignedRoute(
|
|
'admin.tickets.image',
|
|
now()->addDay(),
|
|
['ticket' => $ticket->ticket_num, 'index' => $index],
|
|
),
|
|
array_keys(self::entries($ticket)),
|
|
);
|
|
}
|
|
|
|
public static function moveLegacyToPrivate(Ticket $ticket): bool
|
|
{
|
|
$moved = false;
|
|
|
|
$gallery = array_map(function (string $entry) use ($ticket, &$moved) {
|
|
$location = self::locate($entry);
|
|
|
|
if ($location === null || $location[0] !== self::LEGACY_DISK) {
|
|
return $entry;
|
|
}
|
|
|
|
$target = "ticket-attachments/{$ticket->customer_id}/" . basename($location[1]);
|
|
Storage::disk(self::DISK)->writeStream($target, Storage::disk(self::LEGACY_DISK)->readStream($location[1]));
|
|
Storage::disk(self::LEGACY_DISK)->delete($location[1]);
|
|
$moved = true;
|
|
|
|
return $target;
|
|
}, self::entries($ticket));
|
|
|
|
if ($moved) {
|
|
$ticket->forceFill(['gallery' => $gallery])->saveQuietly();
|
|
}
|
|
|
|
return $moved;
|
|
}
|
|
|
|
public static function response(Ticket $ticket, int $index): StreamedResponse
|
|
{
|
|
$location = self::locate(self::entries($ticket)[$index] ?? null);
|
|
|
|
abort_if($location === null, 404);
|
|
|
|
[$disk, $path] = $location;
|
|
|
|
return Storage::disk($disk)->response($path, null, ['Cache-Control' => 'private, max-age=3600']);
|
|
}
|
|
|
|
/**
|
|
* @return string[]
|
|
*/
|
|
private static function entries(Ticket $ticket): array
|
|
{
|
|
return array_values(array_filter(
|
|
(array) ($ticket->gallery ?? []),
|
|
fn ($entry) => is_string($entry) && $entry !== '',
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @return array{0: string, 1: string}|null
|
|
*/
|
|
private static function locate(?string $entry): ?array
|
|
{
|
|
if ($entry === null) {
|
|
return null;
|
|
}
|
|
|
|
[$disk, $path] = [self::DISK, $entry];
|
|
|
|
if (preg_match('#^https?://#i', $entry)) {
|
|
$urlPath = (string) parse_url($entry, PHP_URL_PATH);
|
|
$offset = strpos($urlPath, self::LEGACY_URL_SEGMENT);
|
|
|
|
if ($offset === false) {
|
|
return null;
|
|
}
|
|
|
|
[$disk, $path] = [self::LEGACY_DISK, rawurldecode(substr($urlPath, $offset + strlen(self::LEGACY_URL_SEGMENT)))];
|
|
}
|
|
|
|
if (str_contains($path, '..') || ! Storage::disk($disk)->exists($path)) {
|
|
return null;
|
|
}
|
|
|
|
return [$disk, $path];
|
|
}
|
|
}
|