diff --git a/CHANGELOG.md b/CHANGELOG.md index 8933387..7e38520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to Domain Monitor will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.4] - 2026-03-02 + +### Added +- **CSV/JSON Import & Export for TLD Registry** - Export all TLDs with WHOIS servers, RDAP servers, registry URLs, and active status; import from CSV/JSON with create-or-update logic and duplicate detection +- **Manual TLD Creation** - Create button with popup modal to add custom TLD entries (supports multi-level TLDs like .co.uk, .co.za, .com.au) +- **IANA Dropdown Menu** - Consolidated "Import TLDs from IANA", "Check for Updates", and "IANA Import Logs" into a single indigo dropdown, reducing button clutter and separating IANA sync from file import/export +- **TldRegistry::findByTld()** - Lookup TLDs regardless of active status (used by import deduplication and create duplicate check) +- **TldRegistry::getAll()** - Retrieve all TLDs ordered by name for export + +### Changed +- **Standardized Import Logging** - Added consistent `Logger('import')` calls across all four import functions (Tags, Domains, Notification Groups, TLD Registry) with start, file info, parse count, validation warnings, and completion stats +- **Standardized Export Logging** - TLD Registry export now uses local `Logger('export')` instances matching Tags, Domains, and Notification Groups pattern +- **TLD Registry Action Bar Redesigned** - Six separate buttons consolidated into four: IANA dropdown (indigo), Export dropdown (emerald), Import button, Create TLD button + +### Technical +- **Drag-and-Drop File Upload for TLD Import** - Same dropzone pattern as Tags and Groups with file preview, remove, and submit spinner +- **TLD Validation** - Regex supports multi-level TLDs (`^\.[a-z0-9\-]+(\.[a-z0-9\-]+)*$`), auto-lowercasing, dot-prefix normalization +- **Import Create-or-Update** - File import creates new TLDs or updates existing ones; RDAP servers parsed from JSON arrays or comma/semicolon-separated strings +- **Routes** - Added `GET /tld-registry/export`, `POST /tld-registry/import`, `POST /tld-registry/create` before `{id}` catch-all + +### Migrations +- `026_update_app_version_v1.1.4.sql` - Updates app version to 1.1.4 + +--- + ## [1.1.3] - 2026-02-11 ### Added @@ -443,8 +468,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [ ] SMS notifications (Twilio) - [x] Google Chat notifications (completed - v1.1.2) - [ ] WhatsApp notifications -- [x] Export functionality (CSV, JSON) (completed - v1.1.3) -- [x] Import domains from CSV/JSON (completed - v1.1.3) +- [x] Export functionality (CSV, JSON) (completed - v1.1.3, TLD Registry - v1.1.4) +- [x] Import domains from CSV/JSON (completed - v1.1.3, TLD Registry - v1.1.4) - [ ] Domain transfer tracking - [ ] DNS record monitoring - [ ] SSL certificate monitoring @@ -466,6 +491,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Version History +### 1.1.4 (2026-03-02) +- **TLD Registry Import & Export** - CSV/JSON export/import for TLD entries with WHOIS, RDAP, registry URL data +- **Manual TLD Creation** - Modal form to add custom TLDs with multi-level support (.co.uk, .co.za, .com.au) +- **IANA Dropdown** - Consolidated IANA operations (Import TLDs, Check Updates, Import Logs) into a single dropdown +- **Standardized Import/Export Logging** - Consistent `Logger` usage across Tags, Domains, Notification Groups, and TLD Registry +- **TLD Registry Action Bar Redesigned** - Cleaner layout: IANA (indigo), Export (emerald), Import, Create TLD +- Migration: `026_update_app_version_v1.1.4.sql` + ### 1.1.3 (2026-02-11) - **CSV/JSON Import & Export** - Domains, Tags, and Notification Groups with drag-and-drop file upload - **Sensitive Data Masking** - API tokens and webhook URLs masked in group exports; masked channels imported as disabled diff --git a/app/Controllers/DomainController.php b/app/Controllers/DomainController.php index 652a919..87b559d 100644 --- a/app/Controllers/DomainController.php +++ b/app/Controllers/DomainController.php @@ -224,16 +224,26 @@ class DomainController extends Controller $this->verifyCsrf('/domains/bulk-add'); + $logger = new \App\Services\Logger('import'); + $userId = \Core\Auth::id(); + $logger->info('Domains import started', ['user_id' => $userId]); + if (!isset($_FILES['import_file']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) { + $logger->warning('No valid file uploaded for domains import'); $_SESSION['error'] = 'Please select a valid file to import'; $this->redirect('/domains/bulk-add'); return; } $file = $_FILES['import_file']; + $logger->info('Import file received', [ + 'filename' => $file['name'], + 'size' => $file['size'] + ]); // Validate file size (5MB max for domains) if ($file['size'] > 5242880) { + $logger->warning('Import file too large', ['size' => $file['size']]); $_SESSION['error'] = 'File is too large. Maximum size is 5MB'; $this->redirect('/domains/bulk-add'); return; @@ -241,6 +251,7 @@ class DomainController extends Controller $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($ext, ['csv', 'json'])) { + $logger->warning('Invalid file type for domains import', ['extension' => $ext]); $_SESSION['error'] = 'Invalid file type. Please upload a CSV or JSON file'; $this->redirect('/domains/bulk-add'); return; @@ -252,6 +263,7 @@ class DomainController extends Controller if ($ext === 'json') { $parsed = json_decode($content, true); if (!is_array($parsed)) { + $logger->error('Invalid JSON file for domains import'); $_SESSION['error'] = 'Invalid JSON file'; $this->redirect('/domains/bulk-add'); return; @@ -275,12 +287,14 @@ class DomainController extends Controller } if (empty($domainsData)) { + $logger->warning('No domains found in import file'); $_SESSION['error'] = 'No domains found in file'; $this->redirect('/domains/bulk-add'); return; } - $userId = \Core\Auth::id(); + $logger->info('Domains data parsed from file', ['entries' => count($domainsData)]); + $settingModel = new \App\Models\Setting(); $isolationMode = $settingModel->getValue('user_isolation_mode', 'shared'); $tagModel = new \App\Models\Tag(); @@ -291,7 +305,6 @@ class DomainController extends Controller $added = 0; $skipped = 0; $errors = []; - $logger = new \App\Services\Logger(); foreach ($domainsData as $row) { $domainName = strtolower(trim($row['domain_name'] ?? '')); @@ -367,6 +380,12 @@ class DomainController extends Controller } } + $logger->info('Domains import completed', [ + 'added' => $added, + 'skipped' => $skipped, + 'failed' => count($errors) + ]); + $msg = "{$added} domain(s) imported successfully"; if ($skipped > 0) $msg .= ", {$skipped} skipped (already exist)"; if (!empty($errors)) $msg .= ", " . count($errors) . " failed"; diff --git a/app/Controllers/InstallerController.php b/app/Controllers/InstallerController.php index 8debead..64fa04c 100644 --- a/app/Controllers/InstallerController.php +++ b/app/Controllers/InstallerController.php @@ -56,6 +56,7 @@ class InstallerController extends Controller '023_update_app_version_to_1.1.1.sql', '024_add_status_notifications_v1.1.2.sql', '025_add_update_system_v1.1.3.sql', + '026_update_app_version_v1.1.4.sql', ]; try { @@ -198,6 +199,7 @@ class InstallerController extends Controller '023_update_app_version_to_1.1.1.sql', '024_add_status_notifications_v1.1.2.sql', '025_add_update_system_v1.1.3.sql', + '026_update_app_version_v1.1.4.sql', ]; } @@ -420,6 +422,7 @@ class InstallerController extends Controller '023_update_app_version_to_1.1.1.sql', '024_add_status_notifications_v1.1.2.sql', '025_add_update_system_v1.1.3.sql', + '026_update_app_version_v1.1.4.sql', ]; $stmt = $pdo->prepare("INSERT INTO migrations (migration) VALUES (?) ON DUPLICATE KEY UPDATE migration=migration"); @@ -655,7 +658,9 @@ class InstallerController extends Controller // Fallback: detect "to" version from which migrations were run if ($toVersion === $fromVersion) { - if (in_array('025_add_update_system_v1.1.3.sql', $executed)) { + if (in_array('026_update_app_version_v1.1.4.sql', $executed)) { + $toVersion = '1.1.4'; + } elseif (in_array('025_add_update_system_v1.1.3.sql', $executed)) { $toVersion = '1.1.3'; } elseif (in_array('024_add_status_notifications_v1.1.2.sql', $executed)) { $toVersion = '1.1.2'; diff --git a/app/Controllers/NotificationGroupController.php b/app/Controllers/NotificationGroupController.php index 00cde84..23860ba 100644 --- a/app/Controllers/NotificationGroupController.php +++ b/app/Controllers/NotificationGroupController.php @@ -194,17 +194,27 @@ class NotificationGroupController extends Controller $this->verifyCsrf('/groups'); + $logger = new \App\Services\Logger('import'); + $userId = \Core\Auth::id(); + $logger->info('Notification groups import started', ['user_id' => $userId]); + $validChannelTypes = ['email', 'telegram', 'discord', 'slack', 'mattermost', 'webhook', 'pushover']; if (!isset($_FILES['import_file']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) { + $logger->warning('No valid file uploaded for groups import'); $_SESSION['error'] = 'Please select a valid file to import'; $this->redirect('/groups'); return; } $file = $_FILES['import_file']; + $logger->info('Import file received', [ + 'filename' => $file['name'], + 'size' => $file['size'] + ]); if ($file['size'] > 2097152) { + $logger->warning('Import file too large', ['size' => $file['size']]); $_SESSION['error'] = 'File is too large. Maximum size is 2MB'; $this->redirect('/groups'); return; @@ -212,13 +222,13 @@ class NotificationGroupController extends Controller $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($ext, ['csv', 'json'])) { + $logger->warning('Invalid file type for groups import', ['extension' => $ext]); $_SESSION['error'] = 'Invalid file type. Please upload a CSV or JSON file'; $this->redirect('/groups'); return; } $content = file_get_contents($file['tmp_name']); - $userId = \Core\Auth::id(); $settingModel = new \App\Models\Setting(); $isolationMode = $settingModel->getValue('user_isolation_mode', 'shared'); @@ -229,11 +239,14 @@ class NotificationGroupController extends Controller if ($ext === 'json') { $parsed = json_decode($content, true); if (!is_array($parsed)) { + $logger->error('Invalid JSON file for groups import'); $_SESSION['error'] = 'Invalid JSON file'; $this->redirect('/groups'); return; } + $logger->info('Groups data parsed from file', ['entries' => count($parsed)]); + foreach ($parsed as $groupData) { $groupName = trim($groupData['group_name'] ?? ''); if (empty($groupName)) continue; @@ -338,6 +351,12 @@ class NotificationGroupController extends Controller } } + $logger->info('Notification groups import completed', [ + 'groups_created' => $groupsCreated, + 'channels_created' => $channelsCreated, + 'groups_skipped' => $groupsSkipped + ]); + $msg = "{$groupsCreated} group(s) imported ({$channelsCreated} channels)"; if ($groupsSkipped > 0) $msg .= ", {$groupsSkipped} skipped (already exist)"; $_SESSION['success'] = $msg; diff --git a/app/Controllers/TagController.php b/app/Controllers/TagController.php index 1c430e3..0aa337e 100644 --- a/app/Controllers/TagController.php +++ b/app/Controllers/TagController.php @@ -176,16 +176,26 @@ class TagController extends Controller $this->verifyCsrf('/tags'); + $logger = new \App\Services\Logger('import'); + $userId = \Core\Auth::id(); + $logger->info('Tags import started', ['user_id' => $userId]); + if (!isset($_FILES['import_file']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) { + $logger->warning('No valid file uploaded for tags import'); $_SESSION['error'] = 'Please select a valid file to import'; $this->redirect('/tags'); return; } $file = $_FILES['import_file']; + $logger->info('Import file received', [ + 'filename' => $file['name'], + 'size' => $file['size'] + ]); // Validate file size (1MB max) if ($file['size'] > 1048576) { + $logger->warning('Import file too large', ['size' => $file['size']]); $_SESSION['error'] = 'File is too large. Maximum size is 1MB'; $this->redirect('/tags'); return; @@ -194,6 +204,7 @@ class TagController extends Controller // Detect format from extension $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($ext, ['csv', 'json'])) { + $logger->warning('Invalid file type for tags import', ['extension' => $ext]); $_SESSION['error'] = 'Invalid file type. Please upload a CSV or JSON file'; $this->redirect('/tags'); return; @@ -205,6 +216,7 @@ class TagController extends Controller if ($ext === 'json') { $parsed = json_decode($content, true); if (!is_array($parsed)) { + $logger->error('Invalid JSON file for tags import'); $_SESSION['error'] = 'Invalid JSON file'; $this->redirect('/tags'); return; @@ -228,12 +240,13 @@ class TagController extends Controller } if (empty($tagsData)) { + $logger->warning('No tags found in import file'); $_SESSION['error'] = 'No tags found in file'; $this->redirect('/tags'); return; } - $userId = \Core\Auth::id(); + $logger->info('Tags data parsed from file', ['entries' => count($tagsData)]); $colorMap = $this->tagModel->getAvailableColors(); // cssClass => 'Name' $availableColorClasses = array_keys($colorMap); // Build reverse map: lowercase name => cssClass (e.g. 'blue' => 'bg-blue-100 ...') @@ -284,6 +297,11 @@ class TagController extends Controller $created++; } + $logger->info('Tags import completed', [ + 'created' => $created, + 'skipped' => $skipped + ]); + $_SESSION['success'] = "{$created} tag(s) imported successfully" . ($skipped > 0 ? ", {$skipped} skipped (already exist or invalid)" : ''); $this->redirect('/tags'); } diff --git a/app/Controllers/TldRegistryController.php b/app/Controllers/TldRegistryController.php index fadc9bd..2eafbf6 100644 --- a/app/Controllers/TldRegistryController.php +++ b/app/Controllers/TldRegistryController.php @@ -640,6 +640,359 @@ class TldRegistryController extends Controller } } + /** + * Export TLD registry as CSV or JSON + */ + public function export() + { + Auth::requireAdmin(); + + $logger = new \App\Services\Logger('export'); + + try { + $format = $_GET['format'] ?? 'csv'; + $logger->info('TLD registry export started', [ + 'format' => $format, + 'user_id' => $_SESSION['user_id'] ?? 'unknown' + ]); + + if (!in_array($format, ['csv', 'json'])) { + $_SESSION['error'] = 'Invalid export format'; + $this->redirect('/tld-registry'); + return; + } + + $allTlds = $this->tldModel->getAll(); + + $tlds = array_map(fn($t) => [ + 'tld' => $t['tld'], + 'whois_server' => $t['whois_server'] ?? '', + 'rdap_servers' => $t['rdap_servers'] ?? '', + 'registry_url' => $t['registry_url'] ?? '', + 'is_active' => $t['is_active'] ? 'yes' : 'no', + ], $allTlds); + + $logger->info('TLD registry export data prepared', ['count' => count($tlds)]); + + $date = date('Y-m-d'); + $filename = "tld_registry_export_{$date}"; + + while (ob_get_level()) { + ob_end_clean(); + } + + if ($format === 'json') { + header('Content-Type: application/json'); + header("Content-Disposition: attachment; filename=\"{$filename}.json\""); + header('Cache-Control: no-cache, must-revalidate'); + header('Pragma: no-cache'); + echo json_encode($tlds, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + } else { + $csvContent = $this->buildCsv($tlds, ['tld', 'whois_server', 'rdap_servers', 'registry_url', 'is_active']); + $logger->info('CSV content built', ['bytes' => strlen($csvContent)]); + + header('Content-Type: text/csv; charset=utf-8'); + header("Content-Disposition: attachment; filename=\"{$filename}.csv\""); + header('Content-Length: ' . strlen($csvContent)); + header('Cache-Control: no-cache, must-revalidate'); + header('Pragma: no-cache'); + echo $csvContent; + } + + $logger->info('TLD registry export completed successfully'); + exit; + + } catch (\Throwable $e) { + $logger->error('TLD registry export failed', [ + 'error' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + $_SESSION['error'] = 'Export failed: ' . $e->getMessage(); + $this->redirect('/tld-registry'); + } + } + + private function buildCsv(array $rows, array $headers): string + { + $handle = fopen('php://temp', 'r+'); + fputcsv($handle, $headers, ',', '"', '\\'); + foreach ($rows as $row) { + fputcsv($handle, array_values($row), ',', '"', '\\'); + } + rewind($handle); + $csv = stream_get_contents($handle); + fclose($handle); + return $csv; + } + + /** + * Import TLDs from CSV or JSON file + */ + public function import() + { + Auth::requireAdmin(); + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/tld-registry'); + return; + } + + $this->verifyCsrf('/tld-registry'); + + $logger = new \App\Services\Logger('import'); + $logger->info('TLD registry import started', [ + 'user_id' => $_SESSION['user_id'] ?? 'unknown', + 'username' => $_SESSION['username'] ?? 'unknown' + ]); + + if (!isset($_FILES['import_file']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) { + $logger->warning('No valid file uploaded for TLD import'); + $_SESSION['error'] = 'Please select a valid file to import'; + $this->redirect('/tld-registry'); + return; + } + + $file = $_FILES['import_file']; + $logger->info('Import file received', [ + 'filename' => $file['name'], + 'size' => $file['size'] + ]); + + if ($file['size'] > 5242880) { + $logger->warning('Import file too large', ['size' => $file['size']]); + $_SESSION['error'] = 'File is too large. Maximum size is 5MB'; + $this->redirect('/tld-registry'); + return; + } + + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if (!in_array($ext, ['csv', 'json'])) { + $logger->warning('Invalid file type for TLD import', ['extension' => $ext]); + $_SESSION['error'] = 'Invalid file type. Please upload a CSV or JSON file'; + $this->redirect('/tld-registry'); + return; + } + + $content = file_get_contents($file['tmp_name']); + $tldsData = []; + + if ($ext === 'json') { + $parsed = json_decode($content, true); + if (!is_array($parsed)) { + $logger->error('Invalid JSON file for TLD import'); + $_SESSION['error'] = 'Invalid JSON file'; + $this->redirect('/tld-registry'); + return; + } + $tldsData = $parsed; + } else { + $lines = array_filter(explode("\n", $content)); + $header = null; + foreach ($lines as $line) { + $row = str_getcsv(trim($line), ',', '"', '\\'); + if (!$header) { + $header = array_map('strtolower', array_map('trim', $row)); + continue; + } + $item = []; + foreach ($header as $i => $col) { + $item[$col] = $row[$i] ?? ''; + } + $tldsData[] = $item; + } + } + + if (empty($tldsData)) { + $logger->warning('No TLD data found in import file'); + $_SESSION['error'] = 'No TLD data found in the file'; + $this->redirect('/tld-registry'); + return; + } + + $logger->info('TLD data parsed from file', ['entries' => count($tldsData)]); + + $imported = 0; + $updated = 0; + $skipped = 0; + + foreach ($tldsData as $tldRow) { + $tldName = trim($tldRow['tld'] ?? ''); + if (empty($tldName)) { + $skipped++; + continue; + } + + if (!str_starts_with($tldName, '.')) { + $tldName = '.' . $tldName; + } + + $existing = $this->tldModel->findByTld($tldName); + + $data = [ + 'tld' => $tldName, + 'updated_at' => date('Y-m-d H:i:s'), + ]; + + if (!empty($tldRow['whois_server'])) { + $data['whois_server'] = trim($tldRow['whois_server']); + } + if (!empty($tldRow['rdap_servers'])) { + $rdap = trim($tldRow['rdap_servers']); + if (str_starts_with($rdap, '[')) { + $data['rdap_servers'] = $rdap; + } else { + $servers = array_filter(array_map('trim', preg_split('/[,;]+/', $rdap))); + $data['rdap_servers'] = json_encode(array_values($servers)); + } + } + if (!empty($tldRow['registry_url'])) { + $data['registry_url'] = trim($tldRow['registry_url']); + } + if (isset($tldRow['is_active'])) { + $val = strtolower(trim($tldRow['is_active'])); + $data['is_active'] = in_array($val, ['yes', '1', 'true', 'active']) ? 1 : 0; + } + + if ($existing) { + unset($data['tld']); + $this->tldModel->update($existing['id'], $data); + $updated++; + } else { + if (!isset($data['is_active'])) { + $data['is_active'] = 1; + } + $data['created_at'] = date('Y-m-d H:i:s'); + $this->tldModel->create($data); + $imported++; + } + } + + $logger->info('TLD registry import completed', [ + 'imported' => $imported, + 'updated' => $updated, + 'skipped' => $skipped + ]); + + $message = "Import completed: {$imported} new, {$updated} updated"; + if ($skipped > 0) { + $message .= ", {$skipped} skipped"; + } + $_SESSION['success'] = $message; + $this->redirect('/tld-registry'); + } + + /** + * Create a new TLD registry entry manually + */ + public function createTld() + { + Auth::requireAdmin(); + + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->redirect('/tld-registry'); + return; + } + + $this->verifyCsrf('/tld-registry'); + + $tldName = trim($_POST['tld'] ?? ''); + $whoisServer = trim($_POST['whois_server'] ?? ''); + $rdapServersInput = trim($_POST['rdap_servers'] ?? ''); + $registryUrl = trim($_POST['registry_url'] ?? ''); + + $this->logger->info('Manual TLD creation requested', [ + 'tld' => $tldName, + 'user_id' => $_SESSION['user_id'] ?? 'unknown', + 'username' => $_SESSION['username'] ?? 'unknown' + ]); + + if (empty($tldName)) { + $_SESSION['error'] = 'TLD name is required'; + $this->redirect('/tld-registry'); + return; + } + + if (!str_starts_with($tldName, '.')) { + $tldName = '.' . $tldName; + } + + $tldName = strtolower($tldName); + + if (!preg_match('/^\.[a-z0-9\-]+(\.[a-z0-9\-]+)*$/', $tldName)) { + $this->logger->warning('Invalid TLD format provided', ['tld' => $tldName]); + $_SESSION['error'] = 'Invalid TLD format. Use only letters, numbers, hyphens, and dots for multi-level TLDs (e.g., .co.uk).'; + $this->redirect('/tld-registry'); + return; + } + + $existing = $this->tldModel->findByTld($tldName); + if ($existing) { + $this->logger->warning('Attempted to create duplicate TLD', ['tld' => $tldName]); + $_SESSION['error'] = "TLD {$tldName} already exists in the registry"; + $this->redirect('/tld-registry'); + return; + } + + if (!empty($whoisServer) && !preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/i', $whoisServer)) { + $_SESSION['error'] = 'Invalid WHOIS server format'; + $this->redirect('/tld-registry'); + return; + } + + $rdapServers = []; + if (!empty($rdapServersInput)) { + $servers = preg_split('/[,\n\r]+/', $rdapServersInput); + foreach ($servers as $server) { + $server = trim($server); + if (!empty($server)) { + $server = rtrim($server, '/') . '/'; + $rdapServers[] = $server; + } + } + } + + try { + $data = [ + 'tld' => $tldName, + 'is_active' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]; + + if (!empty($whoisServer)) { + $data['whois_server'] = $whoisServer; + } + if (!empty($rdapServers)) { + $data['rdap_servers'] = json_encode($rdapServers); + } + if (!empty($registryUrl)) { + $data['registry_url'] = $registryUrl; + } + + $this->tldModel->create($data); + + $this->logger->info('TLD created successfully', [ + 'tld' => $tldName, + 'whois_server' => $whoisServer ?: null, + 'rdap_servers_count' => count($rdapServers), + 'registry_url' => $registryUrl ?: null + ]); + + $_SESSION['success'] = "TLD {$tldName} created successfully"; + } catch (\Exception $e) { + $_SESSION['error'] = 'Failed to create TLD: ' . $e->getMessage(); + $this->logger->error('Failed to create TLD', [ + 'tld' => $tldName, + 'error' => $e->getMessage(), + 'file' => $e->getFile(), + 'line' => $e->getLine() + ]); + } + + $this->redirect('/tld-registry'); + } + /** * Extract WHOIS server from HTML */ diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 9d7b04d..a61f4cd 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -122,7 +122,7 @@ class Setting extends Model */ public function getAppVersion(): string { - return $this->getValue('app_version', '1.1.3'); + return $this->getValue('app_version', '1.1.4'); } /** diff --git a/app/Models/TldRegistry.php b/app/Models/TldRegistry.php index a0f3902..44234cf 100644 --- a/app/Models/TldRegistry.php +++ b/app/Models/TldRegistry.php @@ -242,6 +242,29 @@ class TldRegistry extends Model return $stmt->fetchAll(); } + /** + * Find TLD by extension (regardless of active status) + */ + public function findByTld(string $tld): ?array + { + if (!str_starts_with($tld, '.')) { + $tld = '.' . $tld; + } + + $stmt = $this->db->prepare("SELECT * FROM tld_registry WHERE tld = ?"); + $stmt->execute([$tld]); + return $stmt->fetch() ?: null; + } + + /** + * Get all TLDs (regardless of active status) for export + */ + public function getAll(): array + { + $stmt = $this->db->query("SELECT * FROM tld_registry ORDER BY tld ASC"); + return $stmt->fetchAll(); + } + /** * Execute a custom SQL query */ diff --git a/app/Services/WhoisService.php b/app/Services/WhoisService.php index 59aaf90..8f7b3a0 100644 --- a/app/Services/WhoisService.php +++ b/app/Services/WhoisService.php @@ -938,7 +938,7 @@ class WhoisService } // Registrar (only take the first valid one found) - for standard format - if (!$registrarFound && preg_match('/^registrar(?!.*url|.*whois|.*iana|.*phone|.*email|.*fax|.*abuse|.*id|.*contact)/i', $key) && !empty($value)) { + if (!$registrarFound && preg_match('/^registrar(?!.*url|.*whois|.*iana|.*phone|.*email|.*fax|.*abuse|.*id|.*contact|.*date|.*expir)/i', $key) && !empty($value)) { // Skip if it looks like a phone number, email, or ID if (!preg_match('/^[\+\d\.\s\(\)-]+$/', $value) && !preg_match('/@/', $value) && @@ -1041,6 +1041,11 @@ class WhoisService $dateString = preg_replace('/^(before|after):/i', '', $dateString); $dateString = trim($dateString); + // ISO-8601 format (e.g. ZARC: 2026-10-15T12:00:00Z or 2026-10-15T12:00:00+00:00) + if (preg_match('/^(\d{4}-\d{2}-\d{2})T\d{2}:\d{2}(?::\d{2})?(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?$/i', $dateString, $m)) { + return $m[1]; + } + // Handle DD/MM/YYYY format (European format used by many WHOIS servers like .pt, .es, .fr, etc.) // Pattern: DD/MM/YYYY or DD/MM/YYYY HH:MM:SS if (preg_match('#^(\d{1,2})/(\d{1,2})/(\d{4})(.*)$#', $dateString, $matches)) { diff --git a/app/Views/tld-registry/index.php b/app/Views/tld-registry/index.php index a0e2ab4..ae361f2 100644 --- a/app/Views/tld-registry/index.php +++ b/app/Views/tld-registry/index.php @@ -29,27 +29,65 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc' -
- - - Import Logs - -
- - - -
-
- - - +
+
+ + + +
+ + + IANA Import Logs + +
+ + +
+ - + +
+ + + +
@@ -529,6 +567,122 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
+ + + + + + post('/channels/test', [NotificationGroupController::class, 'testChanne // TLD Registry $router->get('/tld-registry', [TldRegistryController::class, 'index']); +$router->get('/tld-registry/export', [TldRegistryController::class, 'export']); +$router->post('/tld-registry/import', [TldRegistryController::class, 'import']); +$router->post('/tld-registry/create', [TldRegistryController::class, 'createTld']); $router->get('/tld-registry/{id}', [TldRegistryController::class, 'show']); $router->post('/tld-registry/import-tld-list', [TldRegistryController::class, 'importTldList']); $router->post('/tld-registry/import-rdap', [TldRegistryController::class, 'importRdap']);