- Lịch sử thanh toán hoá đơn trả thêm `method` (tự động gia hạn / số dư / admin / chuyển khoản / hoàn tiền), bỏ đuôi `(method)` khỏi ghi chú - `DnsServer::panelUrl()` = https://<domain>/ dùng cho email cấp hosting và nút Plesk của khách - Order item trả thêm `uuid`/`type` của dịch vụ để FE dẫn tới trang quản lý - Yêu cầu huỷ dịch vụ, chứng chỉ SSL, lịch sử gia hạn, đổi mật khẩu hosting cho khách Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
68 lines
2.0 KiB
PHP
68 lines
2.0 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Models\ServiceSslCertificate;
|
|
use Illuminate\Http\UploadedFile;
|
|
use InvalidArgumentException;
|
|
use ZipArchive;
|
|
|
|
class SslBundleExtractor
|
|
{
|
|
private const MAX_TOTAL_BYTES = 2097152;
|
|
|
|
public static function extract(UploadedFile $archive): array
|
|
{
|
|
$zip = new ZipArchive();
|
|
if ($zip->open($archive->getRealPath(), ZipArchive::RDONLY) !== true) {
|
|
throw new InvalidArgumentException('Không đọc được file zip.');
|
|
}
|
|
|
|
$files = [];
|
|
$total = 0;
|
|
|
|
for ($index = 0; $index < $zip->numFiles; $index++) {
|
|
$name = self::cleanName($zip->getNameIndex($index));
|
|
if ($name === null) {
|
|
continue;
|
|
}
|
|
|
|
$content = $zip->getFromIndex($index, ServiceSslCertificate::MAX_FILE_BYTES + 1);
|
|
if ($content === false || strlen($content) > ServiceSslCertificate::MAX_FILE_BYTES || ! self::isText($content)) {
|
|
continue;
|
|
}
|
|
|
|
$total += strlen($content);
|
|
if ($total > self::MAX_TOTAL_BYTES || count($files) >= ServiceSslCertificate::MAX_ENTRIES) {
|
|
throw new InvalidArgumentException('File zip chứa quá nhiều dữ liệu.');
|
|
}
|
|
|
|
$files[$name] = ['name' => $name, 'content' => $content];
|
|
}
|
|
|
|
$zip->close();
|
|
|
|
if ($files === []) {
|
|
throw new InvalidArgumentException('File zip không có tệp văn bản nào (pem, crt, key, txt...).');
|
|
}
|
|
|
|
return array_values($files);
|
|
}
|
|
|
|
private static function cleanName(string $entry): ?string
|
|
{
|
|
if (str_ends_with($entry, '/') || str_contains($entry, '__MACOSX/')) {
|
|
return null;
|
|
}
|
|
|
|
$name = trim(basename($entry));
|
|
|
|
return $name === '' || str_starts_with($name, '.') ? null : mb_substr($name, 0, 150);
|
|
}
|
|
|
|
private static function isText(string $content): bool
|
|
{
|
|
return ! str_contains($content, "\0") && mb_check_encoding($content, 'UTF-8');
|
|
}
|
|
}
|