256 lines
7.4 KiB
PHP
Executable File
256 lines
7.4 KiB
PHP
Executable File
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Modules\Helpdesk\Models;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
final class Attachment extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
protected $table = 'helpdesk_attachments';
|
|
|
|
/** @var list<string> */
|
|
protected $fillable = [
|
|
'ticket_id',
|
|
'comment_id',
|
|
'user_id',
|
|
'disk',
|
|
'path',
|
|
'original_name',
|
|
'mime_type',
|
|
'size',
|
|
'extension',
|
|
'category',
|
|
'checksum_sha256',
|
|
'download_count',
|
|
'last_downloaded_at',
|
|
'deleted_at',
|
|
'deleted_by',
|
|
];
|
|
|
|
/** @var array<string, string> */
|
|
protected $casts = [
|
|
'size' => 'integer',
|
|
'download_count' => 'integer',
|
|
'last_downloaded_at' => 'datetime',
|
|
'deleted_at' => 'datetime',
|
|
];
|
|
|
|
/** @return BelongsTo<Ticket, Attachment> */
|
|
public function ticket(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Ticket::class, 'ticket_id');
|
|
}
|
|
|
|
/** @return BelongsTo<Comment, Attachment> */
|
|
public function comment(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Comment::class, 'comment_id');
|
|
}
|
|
|
|
/** @return BelongsTo<User, Attachment> */
|
|
public function user(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'user_id');
|
|
}
|
|
|
|
/**
|
|
* Return an attribute only when it was actually selected from the database.
|
|
*
|
|
* Laravel 13 can throw when accessing missing attributes. The Helpdesk
|
|
* attachment metadata columns are introduced by an optional forward
|
|
* migration, so model helpers must remain safe before that migration has
|
|
* been applied.
|
|
*/
|
|
private function loadedAttribute(string $key): mixed
|
|
{
|
|
$attributes = $this->getAttributes();
|
|
|
|
return array_key_exists($key, $attributes) ? $this->getAttributeValue($key) : null;
|
|
}
|
|
|
|
/**
|
|
* Return the original client filename with compatibility fallbacks.
|
|
*/
|
|
public function displayName(): string
|
|
{
|
|
$name = $this->loadedAttribute('original_name')
|
|
?? $this->loadedAttribute('original_filename')
|
|
?? $this->loadedAttribute('filename')
|
|
?? $this->loadedAttribute('file_name');
|
|
|
|
if (is_string($name) && $name !== '') {
|
|
return $name;
|
|
}
|
|
|
|
return basename((string) ($this->loadedAttribute('path') ?? $this->loadedAttribute('file_path') ?? 'attachment'));
|
|
}
|
|
|
|
/**
|
|
* Return the stored byte size, falling back to legacy names.
|
|
*/
|
|
public function byteSize(): int
|
|
{
|
|
return (int) ($this->loadedAttribute('size') ?? $this->loadedAttribute('size_bytes') ?? 0);
|
|
}
|
|
|
|
/**
|
|
* Return the safe download count when the column exists.
|
|
*/
|
|
public function downloadCount(): int
|
|
{
|
|
return (int) ($this->loadedAttribute('download_count') ?? 0);
|
|
}
|
|
|
|
/**
|
|
* Determine whether the attachment has been soft deleted.
|
|
*/
|
|
public function isDeletedAttachment(): bool
|
|
{
|
|
return filled($this->loadedAttribute('deleted_at'));
|
|
}
|
|
|
|
/**
|
|
* Return the lower-case file extension from stored metadata or filename.
|
|
*/
|
|
public function extension(): string
|
|
{
|
|
$extension = $this->loadedAttribute('extension');
|
|
|
|
if (! is_string($extension) || $extension === '') {
|
|
$extension = pathinfo($this->displayName(), PATHINFO_EXTENSION);
|
|
}
|
|
|
|
return strtolower(ltrim((string) $extension, '.'));
|
|
}
|
|
|
|
/**
|
|
* Return the display category used by Helpdesk attachment cards.
|
|
*/
|
|
public function category(): string
|
|
{
|
|
$category = $this->loadedAttribute('category');
|
|
|
|
if (is_string($category) && $category !== '') {
|
|
return $category;
|
|
}
|
|
|
|
return match (true) {
|
|
$this->isImage() => 'image',
|
|
$this->isPdf() => 'pdf',
|
|
$this->isArchive() => 'archive',
|
|
$this->isSourceCode() => 'source',
|
|
default => 'file',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determine whether the attachment is a browser-previewable image.
|
|
*/
|
|
public function isImage(): bool
|
|
{
|
|
return in_array($this->extension(), ['png', 'jpg', 'jpeg', 'webp', 'gif'], true)
|
|
|| Str::startsWith((string) $this->loadedAttribute('mime_type'), 'image/');
|
|
}
|
|
|
|
/**
|
|
* Determine whether the attachment is a PDF document.
|
|
*/
|
|
public function isPdf(): bool
|
|
{
|
|
return $this->extension() === 'pdf' || $this->loadedAttribute('mime_type') === 'application/pdf';
|
|
}
|
|
|
|
/**
|
|
* Determine whether the attachment is an archive.
|
|
*/
|
|
public function isArchive(): bool
|
|
{
|
|
return in_array($this->extension(), ['zip', 'tar', 'gz', '7z', 'rar'], true);
|
|
}
|
|
|
|
/**
|
|
* Determine whether the attachment is source/configuration text safe for the read-only viewer.
|
|
*/
|
|
public function isSourceCode(): bool
|
|
{
|
|
return in_array($this->extension(), [
|
|
'php', 'phtml', 'phps', 'blade.php', 'js', 'jsx', 'ts', 'tsx', 'vue', 'css', 'scss', 'sass', 'less',
|
|
'html', 'htm', 'xml', 'json', 'yaml', 'yml', 'sql', 'sh', 'bash', 'zsh', 'ps1', 'bat', 'cmd',
|
|
'py', 'rb', 'go', 'rs', 'java', 'kt', 'kts', 'cs', 'cpp', 'c', 'h', 'hpp', 'swift', 'dart',
|
|
'rpg', 'rpgle', 'cl', 'clle', 'cbl', 'cob', 'copybook', 'inc', 'env', 'ini', 'conf', 'config', 'txt', 'log', 'md',
|
|
], true);
|
|
}
|
|
|
|
/**
|
|
* Determine whether the attachment can be streamed inline by the preview route.
|
|
*/
|
|
public function isPreviewableBinary(): bool
|
|
{
|
|
return $this->isImage() || $this->isPdf();
|
|
}
|
|
|
|
/**
|
|
* Determine whether the Helpdesk UI can show an inline preview or viewer link.
|
|
*/
|
|
public function canPreview(): bool
|
|
{
|
|
return $this->isSourceCode() || $this->isPreviewableBinary();
|
|
}
|
|
|
|
/**
|
|
* Return a Font Awesome icon for the attachment type.
|
|
*/
|
|
public function icon(): string
|
|
{
|
|
return match ($this->category()) {
|
|
'image' => 'fa-regular fa-file-image',
|
|
'pdf' => 'fa-regular fa-file-pdf',
|
|
'source' => 'fa-solid fa-code',
|
|
'archive' => 'fa-regular fa-file-zipper',
|
|
default => 'fa-regular fa-file-lines',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Return the syntax language key used by the source viewer.
|
|
*/
|
|
public function language(): string
|
|
{
|
|
return match ($this->extension()) {
|
|
'php', 'phtml', 'phps', 'blade.php' => 'php',
|
|
'js', 'jsx' => 'javascript',
|
|
'ts', 'tsx' => 'typescript',
|
|
'vue' => 'vue',
|
|
'css', 'scss', 'sass', 'less' => 'css',
|
|
'html', 'htm' => 'html',
|
|
'xml' => 'xml',
|
|
'json' => 'json',
|
|
'yaml', 'yml' => 'yaml',
|
|
'sql' => 'sql',
|
|
'sh', 'bash', 'zsh' => 'shell',
|
|
'ps1' => 'powershell',
|
|
'py' => 'python',
|
|
'java', 'kt', 'kts' => 'java',
|
|
'c', 'h', 'cpp', 'hpp', 'cs' => 'c-like',
|
|
'rpg', 'rpgle' => 'rpgle',
|
|
'cl', 'clle' => 'clle',
|
|
'cbl', 'cob', 'copybook' => 'cobol',
|
|
default => 'text',
|
|
};
|
|
}
|
|
|
|
public function url(): string
|
|
{
|
|
return Storage::disk((string) $this->loadedAttribute('disk'))->url((string) $this->loadedAttribute('path'));
|
|
}
|
|
}
|