Add TLD registry import/export/create & logging
Add CSV/JSON export and import endpoints and UI for the TLD registry, plus a manual Create TLD modal and drag-and-drop import UX. Standardize import/export logging by adding Logger('import'/'export') calls to Domains, Tags, Notification Groups and TLD flows. Add TldRegistry model helpers (findByTld, getAll) used for deduplication and exports. Update routes for /tld-registry export/import/create and add a migration to bump app_version to 1.1.4. Also update default app_version, enhance WhoisService parsing (registrar regex and ISO-8601 date handling), and adjust the TLD registry index view to include IANA and Export dropdowns, import modal, create modal, and related JS behavior.
This commit is contained in:
37
CHANGELOG.md
37
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/),
|
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).
|
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
|
## [1.1.3] - 2026-02-11
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -443,8 +468,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- [ ] SMS notifications (Twilio)
|
- [ ] SMS notifications (Twilio)
|
||||||
- [x] Google Chat notifications (completed - v1.1.2)
|
- [x] Google Chat notifications (completed - v1.1.2)
|
||||||
- [ ] WhatsApp notifications
|
- [ ] WhatsApp notifications
|
||||||
- [x] Export functionality (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)
|
- [x] Import domains from CSV/JSON (completed - v1.1.3, TLD Registry - v1.1.4)
|
||||||
- [ ] Domain transfer tracking
|
- [ ] Domain transfer tracking
|
||||||
- [ ] DNS record monitoring
|
- [ ] DNS record monitoring
|
||||||
- [ ] SSL certificate monitoring
|
- [ ] SSL certificate monitoring
|
||||||
@@ -466,6 +491,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Version History
|
## 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)
|
### 1.1.3 (2026-02-11)
|
||||||
- **CSV/JSON Import & Export** - Domains, Tags, and Notification Groups with drag-and-drop file upload
|
- **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
|
- **Sensitive Data Masking** - API tokens and webhook URLs masked in group exports; masked channels imported as disabled
|
||||||
|
|||||||
@@ -224,16 +224,26 @@ class DomainController extends Controller
|
|||||||
|
|
||||||
$this->verifyCsrf('/domains/bulk-add');
|
$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) {
|
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';
|
$_SESSION['error'] = 'Please select a valid file to import';
|
||||||
$this->redirect('/domains/bulk-add');
|
$this->redirect('/domains/bulk-add');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$file = $_FILES['import_file'];
|
$file = $_FILES['import_file'];
|
||||||
|
$logger->info('Import file received', [
|
||||||
|
'filename' => $file['name'],
|
||||||
|
'size' => $file['size']
|
||||||
|
]);
|
||||||
|
|
||||||
// Validate file size (5MB max for domains)
|
// Validate file size (5MB max for domains)
|
||||||
if ($file['size'] > 5242880) {
|
if ($file['size'] > 5242880) {
|
||||||
|
$logger->warning('Import file too large', ['size' => $file['size']]);
|
||||||
$_SESSION['error'] = 'File is too large. Maximum size is 5MB';
|
$_SESSION['error'] = 'File is too large. Maximum size is 5MB';
|
||||||
$this->redirect('/domains/bulk-add');
|
$this->redirect('/domains/bulk-add');
|
||||||
return;
|
return;
|
||||||
@@ -241,6 +251,7 @@ class DomainController extends Controller
|
|||||||
|
|
||||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
if (!in_array($ext, ['csv', 'json'])) {
|
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';
|
$_SESSION['error'] = 'Invalid file type. Please upload a CSV or JSON file';
|
||||||
$this->redirect('/domains/bulk-add');
|
$this->redirect('/domains/bulk-add');
|
||||||
return;
|
return;
|
||||||
@@ -252,6 +263,7 @@ class DomainController extends Controller
|
|||||||
if ($ext === 'json') {
|
if ($ext === 'json') {
|
||||||
$parsed = json_decode($content, true);
|
$parsed = json_decode($content, true);
|
||||||
if (!is_array($parsed)) {
|
if (!is_array($parsed)) {
|
||||||
|
$logger->error('Invalid JSON file for domains import');
|
||||||
$_SESSION['error'] = 'Invalid JSON file';
|
$_SESSION['error'] = 'Invalid JSON file';
|
||||||
$this->redirect('/domains/bulk-add');
|
$this->redirect('/domains/bulk-add');
|
||||||
return;
|
return;
|
||||||
@@ -275,12 +287,14 @@ class DomainController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (empty($domainsData)) {
|
if (empty($domainsData)) {
|
||||||
|
$logger->warning('No domains found in import file');
|
||||||
$_SESSION['error'] = 'No domains found in file';
|
$_SESSION['error'] = 'No domains found in file';
|
||||||
$this->redirect('/domains/bulk-add');
|
$this->redirect('/domains/bulk-add');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$userId = \Core\Auth::id();
|
$logger->info('Domains data parsed from file', ['entries' => count($domainsData)]);
|
||||||
|
|
||||||
$settingModel = new \App\Models\Setting();
|
$settingModel = new \App\Models\Setting();
|
||||||
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
||||||
$tagModel = new \App\Models\Tag();
|
$tagModel = new \App\Models\Tag();
|
||||||
@@ -291,7 +305,6 @@ class DomainController extends Controller
|
|||||||
$added = 0;
|
$added = 0;
|
||||||
$skipped = 0;
|
$skipped = 0;
|
||||||
$errors = [];
|
$errors = [];
|
||||||
$logger = new \App\Services\Logger();
|
|
||||||
|
|
||||||
foreach ($domainsData as $row) {
|
foreach ($domainsData as $row) {
|
||||||
$domainName = strtolower(trim($row['domain_name'] ?? ''));
|
$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";
|
$msg = "{$added} domain(s) imported successfully";
|
||||||
if ($skipped > 0) $msg .= ", {$skipped} skipped (already exist)";
|
if ($skipped > 0) $msg .= ", {$skipped} skipped (already exist)";
|
||||||
if (!empty($errors)) $msg .= ", " . count($errors) . " failed";
|
if (!empty($errors)) $msg .= ", " . count($errors) . " failed";
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class InstallerController extends Controller
|
|||||||
'023_update_app_version_to_1.1.1.sql',
|
'023_update_app_version_to_1.1.1.sql',
|
||||||
'024_add_status_notifications_v1.1.2.sql',
|
'024_add_status_notifications_v1.1.2.sql',
|
||||||
'025_add_update_system_v1.1.3.sql',
|
'025_add_update_system_v1.1.3.sql',
|
||||||
|
'026_update_app_version_v1.1.4.sql',
|
||||||
];
|
];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -198,6 +199,7 @@ class InstallerController extends Controller
|
|||||||
'023_update_app_version_to_1.1.1.sql',
|
'023_update_app_version_to_1.1.1.sql',
|
||||||
'024_add_status_notifications_v1.1.2.sql',
|
'024_add_status_notifications_v1.1.2.sql',
|
||||||
'025_add_update_system_v1.1.3.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',
|
'023_update_app_version_to_1.1.1.sql',
|
||||||
'024_add_status_notifications_v1.1.2.sql',
|
'024_add_status_notifications_v1.1.2.sql',
|
||||||
'025_add_update_system_v1.1.3.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");
|
$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
|
// Fallback: detect "to" version from which migrations were run
|
||||||
if ($toVersion === $fromVersion) {
|
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';
|
$toVersion = '1.1.3';
|
||||||
} elseif (in_array('024_add_status_notifications_v1.1.2.sql', $executed)) {
|
} elseif (in_array('024_add_status_notifications_v1.1.2.sql', $executed)) {
|
||||||
$toVersion = '1.1.2';
|
$toVersion = '1.1.2';
|
||||||
|
|||||||
@@ -194,17 +194,27 @@ class NotificationGroupController extends Controller
|
|||||||
|
|
||||||
$this->verifyCsrf('/groups');
|
$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'];
|
$validChannelTypes = ['email', 'telegram', 'discord', 'slack', 'mattermost', 'webhook', 'pushover'];
|
||||||
|
|
||||||
if (!isset($_FILES['import_file']) || $_FILES['import_file']['error'] !== UPLOAD_ERR_OK) {
|
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';
|
$_SESSION['error'] = 'Please select a valid file to import';
|
||||||
$this->redirect('/groups');
|
$this->redirect('/groups');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$file = $_FILES['import_file'];
|
$file = $_FILES['import_file'];
|
||||||
|
$logger->info('Import file received', [
|
||||||
|
'filename' => $file['name'],
|
||||||
|
'size' => $file['size']
|
||||||
|
]);
|
||||||
|
|
||||||
if ($file['size'] > 2097152) {
|
if ($file['size'] > 2097152) {
|
||||||
|
$logger->warning('Import file too large', ['size' => $file['size']]);
|
||||||
$_SESSION['error'] = 'File is too large. Maximum size is 2MB';
|
$_SESSION['error'] = 'File is too large. Maximum size is 2MB';
|
||||||
$this->redirect('/groups');
|
$this->redirect('/groups');
|
||||||
return;
|
return;
|
||||||
@@ -212,13 +222,13 @@ class NotificationGroupController extends Controller
|
|||||||
|
|
||||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
if (!in_array($ext, ['csv', 'json'])) {
|
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';
|
$_SESSION['error'] = 'Invalid file type. Please upload a CSV or JSON file';
|
||||||
$this->redirect('/groups');
|
$this->redirect('/groups');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$content = file_get_contents($file['tmp_name']);
|
$content = file_get_contents($file['tmp_name']);
|
||||||
$userId = \Core\Auth::id();
|
|
||||||
$settingModel = new \App\Models\Setting();
|
$settingModel = new \App\Models\Setting();
|
||||||
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
$isolationMode = $settingModel->getValue('user_isolation_mode', 'shared');
|
||||||
|
|
||||||
@@ -229,11 +239,14 @@ class NotificationGroupController extends Controller
|
|||||||
if ($ext === 'json') {
|
if ($ext === 'json') {
|
||||||
$parsed = json_decode($content, true);
|
$parsed = json_decode($content, true);
|
||||||
if (!is_array($parsed)) {
|
if (!is_array($parsed)) {
|
||||||
|
$logger->error('Invalid JSON file for groups import');
|
||||||
$_SESSION['error'] = 'Invalid JSON file';
|
$_SESSION['error'] = 'Invalid JSON file';
|
||||||
$this->redirect('/groups');
|
$this->redirect('/groups');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$logger->info('Groups data parsed from file', ['entries' => count($parsed)]);
|
||||||
|
|
||||||
foreach ($parsed as $groupData) {
|
foreach ($parsed as $groupData) {
|
||||||
$groupName = trim($groupData['group_name'] ?? '');
|
$groupName = trim($groupData['group_name'] ?? '');
|
||||||
if (empty($groupName)) continue;
|
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)";
|
$msg = "{$groupsCreated} group(s) imported ({$channelsCreated} channels)";
|
||||||
if ($groupsSkipped > 0) $msg .= ", {$groupsSkipped} skipped (already exist)";
|
if ($groupsSkipped > 0) $msg .= ", {$groupsSkipped} skipped (already exist)";
|
||||||
$_SESSION['success'] = $msg;
|
$_SESSION['success'] = $msg;
|
||||||
|
|||||||
@@ -176,16 +176,26 @@ class TagController extends Controller
|
|||||||
|
|
||||||
$this->verifyCsrf('/tags');
|
$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) {
|
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';
|
$_SESSION['error'] = 'Please select a valid file to import';
|
||||||
$this->redirect('/tags');
|
$this->redirect('/tags');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$file = $_FILES['import_file'];
|
$file = $_FILES['import_file'];
|
||||||
|
$logger->info('Import file received', [
|
||||||
|
'filename' => $file['name'],
|
||||||
|
'size' => $file['size']
|
||||||
|
]);
|
||||||
|
|
||||||
// Validate file size (1MB max)
|
// Validate file size (1MB max)
|
||||||
if ($file['size'] > 1048576) {
|
if ($file['size'] > 1048576) {
|
||||||
|
$logger->warning('Import file too large', ['size' => $file['size']]);
|
||||||
$_SESSION['error'] = 'File is too large. Maximum size is 1MB';
|
$_SESSION['error'] = 'File is too large. Maximum size is 1MB';
|
||||||
$this->redirect('/tags');
|
$this->redirect('/tags');
|
||||||
return;
|
return;
|
||||||
@@ -194,6 +204,7 @@ class TagController extends Controller
|
|||||||
// Detect format from extension
|
// Detect format from extension
|
||||||
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
|
||||||
if (!in_array($ext, ['csv', 'json'])) {
|
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';
|
$_SESSION['error'] = 'Invalid file type. Please upload a CSV or JSON file';
|
||||||
$this->redirect('/tags');
|
$this->redirect('/tags');
|
||||||
return;
|
return;
|
||||||
@@ -205,6 +216,7 @@ class TagController extends Controller
|
|||||||
if ($ext === 'json') {
|
if ($ext === 'json') {
|
||||||
$parsed = json_decode($content, true);
|
$parsed = json_decode($content, true);
|
||||||
if (!is_array($parsed)) {
|
if (!is_array($parsed)) {
|
||||||
|
$logger->error('Invalid JSON file for tags import');
|
||||||
$_SESSION['error'] = 'Invalid JSON file';
|
$_SESSION['error'] = 'Invalid JSON file';
|
||||||
$this->redirect('/tags');
|
$this->redirect('/tags');
|
||||||
return;
|
return;
|
||||||
@@ -228,12 +240,13 @@ class TagController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (empty($tagsData)) {
|
if (empty($tagsData)) {
|
||||||
|
$logger->warning('No tags found in import file');
|
||||||
$_SESSION['error'] = 'No tags found in file';
|
$_SESSION['error'] = 'No tags found in file';
|
||||||
$this->redirect('/tags');
|
$this->redirect('/tags');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$userId = \Core\Auth::id();
|
$logger->info('Tags data parsed from file', ['entries' => count($tagsData)]);
|
||||||
$colorMap = $this->tagModel->getAvailableColors(); // cssClass => 'Name'
|
$colorMap = $this->tagModel->getAvailableColors(); // cssClass => 'Name'
|
||||||
$availableColorClasses = array_keys($colorMap);
|
$availableColorClasses = array_keys($colorMap);
|
||||||
// Build reverse map: lowercase name => cssClass (e.g. 'blue' => 'bg-blue-100 ...')
|
// Build reverse map: lowercase name => cssClass (e.g. 'blue' => 'bg-blue-100 ...')
|
||||||
@@ -284,6 +297,11 @@ class TagController extends Controller
|
|||||||
$created++;
|
$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)" : '');
|
$_SESSION['success'] = "{$created} tag(s) imported successfully" . ($skipped > 0 ? ", {$skipped} skipped (already exist or invalid)" : '');
|
||||||
$this->redirect('/tags');
|
$this->redirect('/tags');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* Extract WHOIS server from HTML
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class Setting extends Model
|
|||||||
*/
|
*/
|
||||||
public function getAppVersion(): string
|
public function getAppVersion(): string
|
||||||
{
|
{
|
||||||
return $this->getValue('app_version', '1.1.3');
|
return $this->getValue('app_version', '1.1.4');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -242,6 +242,29 @@ class TldRegistry extends Model
|
|||||||
return $stmt->fetchAll();
|
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
|
* Execute a custom SQL query
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -938,7 +938,7 @@ class WhoisService
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Registrar (only take the first valid one found) - for standard format
|
// 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
|
// Skip if it looks like a phone number, email, or ID
|
||||||
if (!preg_match('/^[\+\d\.\s\(\)-]+$/', $value) &&
|
if (!preg_match('/^[\+\d\.\s\(\)-]+$/', $value) &&
|
||||||
!preg_match('/@/', $value) &&
|
!preg_match('/@/', $value) &&
|
||||||
@@ -1041,6 +1041,11 @@ class WhoisService
|
|||||||
$dateString = preg_replace('/^(before|after):/i', '', $dateString);
|
$dateString = preg_replace('/^(before|after):/i', '', $dateString);
|
||||||
$dateString = trim($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.)
|
// 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
|
// Pattern: DD/MM/YYYY or DD/MM/YYYY HH:MM:SS
|
||||||
if (preg_match('#^(\d{1,2})/(\d{1,2})/(\d{4})(.*)$#', $dateString, $matches)) {
|
if (preg_match('#^(\d{1,2})/(\d{1,2})/(\d{4})(.*)$#', $dateString, $matches)) {
|
||||||
|
|||||||
@@ -29,27 +29,65 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
|
|||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
|
<?php if (isset($_SESSION['role']) && $_SESSION['role'] === 'admin'): ?>
|
||||||
<div class="mb-4 flex justify-end gap-2">
|
<div class="mb-4 flex flex-wrap gap-2 justify-end">
|
||||||
<a href="/tld-registry/import-logs" class="inline-flex items-center px-4 py-2 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors font-medium">
|
<!-- IANA Dropdown -->
|
||||||
<i class="fas fa-history mr-2"></i>
|
<div class="relative" id="ianaDropdownWrapper">
|
||||||
Import Logs
|
<button onclick="document.getElementById('ianaDropdownMenu').classList.toggle('hidden')" class="inline-flex items-center px-4 py-2 bg-indigo-600 text-white text-sm rounded-lg hover:bg-indigo-700 transition-colors font-medium">
|
||||||
</a>
|
<i class="fas fa-globe mr-2"></i>
|
||||||
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
|
IANA
|
||||||
<?= csrf_field() ?>
|
<i class="fas fa-chevron-down ml-2 text-xs"></i>
|
||||||
<input type="hidden" name="import_type" value="check_updates">
|
|
||||||
<button type="submit" <?= $tldStats['total'] == 0 ? 'disabled' : '' ?> class="inline-flex items-center px-4 py-2 <?= $tldStats['total'] == 0 ? 'bg-gray-400 cursor-not-allowed' : 'bg-primary hover:bg-primary-dark' ?> text-white text-sm rounded-lg transition-colors font-medium" title="<?= $tldStats['total'] == 0 ? 'Import TLDs first' : 'Check for IANA updates' ?>">
|
|
||||||
<i class="fas fa-sync-alt mr-2"></i>
|
|
||||||
Check Updates
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
<div id="ianaDropdownMenu" class="hidden absolute right-0 mt-1 w-56 bg-white rounded-lg shadow-lg border border-gray-200 z-30 overflow-hidden">
|
||||||
<form method="POST" action="/tld-registry/start-progressive-import" class="inline">
|
<form method="POST" action="/tld-registry/start-progressive-import">
|
||||||
<?= csrf_field() ?>
|
<?= csrf_field() ?>
|
||||||
<input type="hidden" name="import_type" value="complete_workflow">
|
<input type="hidden" name="import_type" value="complete_workflow">
|
||||||
<button type="submit" class="inline-flex items-center px-4 py-2 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium" title="Complete TLD import workflow: TLD List → RDAP → WHOIS → Registry URLs">
|
<button type="submit" class="w-full flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors" title="Complete TLD import workflow: TLD List → RDAP → WHOIS → Registry URLs">
|
||||||
<i class="fas fa-rocket mr-2"></i>
|
<i class="fas fa-rocket text-indigo-600 mr-2.5"></i>
|
||||||
Import TLDs
|
Import TLDs from IANA
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
<form method="POST" action="/tld-registry/start-progressive-import">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<input type="hidden" name="import_type" value="check_updates">
|
||||||
|
<button type="submit" <?= $tldStats['total'] == 0 ? 'disabled' : '' ?> class="w-full flex items-center px-4 py-2.5 text-sm <?= $tldStats['total'] == 0 ? 'text-gray-400 cursor-not-allowed' : 'text-gray-700 hover:bg-gray-50' ?> transition-colors border-t border-gray-100" title="<?= $tldStats['total'] == 0 ? 'Import TLDs first' : 'Check for IANA updates' ?>">
|
||||||
|
<i class="fas fa-sync-alt text-blue-600 mr-2.5"></i>
|
||||||
|
Check for Updates
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<a href="/tld-registry/import-logs" class="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors border-t border-gray-100">
|
||||||
|
<i class="fas fa-history text-gray-500 mr-2.5"></i>
|
||||||
|
IANA Import Logs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Export Dropdown -->
|
||||||
|
<div class="relative" id="tldExportDropdownWrapper">
|
||||||
|
<button onclick="document.getElementById('tldExportDropdownMenu').classList.toggle('hidden')" class="inline-flex items-center px-4 py-2 bg-emerald-600 text-white text-sm rounded-lg hover:bg-emerald-700 transition-colors font-medium">
|
||||||
|
<i class="fas fa-download mr-2"></i>
|
||||||
|
Export
|
||||||
|
<i class="fas fa-chevron-down ml-2 text-xs"></i>
|
||||||
|
</button>
|
||||||
|
<div id="tldExportDropdownMenu" class="hidden absolute right-0 mt-1 w-44 bg-white rounded-lg shadow-lg border border-gray-200 z-30 overflow-hidden">
|
||||||
|
<a href="/tld-registry/export?format=csv" class="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors">
|
||||||
|
<i class="fas fa-file-csv text-green-600 mr-2.5"></i>
|
||||||
|
Export as CSV
|
||||||
|
</a>
|
||||||
|
<a href="/tld-registry/export?format=json" class="flex items-center px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-50 transition-colors border-t border-gray-100">
|
||||||
|
<i class="fas fa-file-code text-blue-600 mr-2.5"></i>
|
||||||
|
Export as JSON
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Import Button -->
|
||||||
|
<button onclick="document.getElementById('tldImportModal').classList.remove('hidden')" class="inline-flex items-center px-4 py-2 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
||||||
|
<i class="fas fa-upload mr-2"></i>
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
<!-- Create Button -->
|
||||||
|
<button onclick="openCreateTldModal()" class="inline-flex items-center px-4 py-2 bg-primary text-white text-sm rounded-lg hover:bg-primary-dark transition-colors font-medium">
|
||||||
|
<i class="fas fa-plus mr-2"></i>
|
||||||
|
Create TLD
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<div class="mb-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
<div class="mb-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
|
||||||
@@ -529,6 +567,122 @@ $currentFilters = $filters ?? ['search' => '', 'sort' => 'tld', 'order' => 'asc'
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Create TLD Modal -->
|
||||||
|
<div id="createTldModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden z-50">
|
||||||
|
<div class="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||||
|
<form method="POST" action="/tld-registry/create">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">Create New TLD</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-6 py-4 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="create_tld" class="block text-sm font-medium text-gray-700 mb-1">TLD Name</label>
|
||||||
|
<input type="text" id="create_tld" name="tld" required
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="e.g., .com, .xyz, .co.uk">
|
||||||
|
<p class="text-xs text-gray-500 mt-1">The dot prefix will be added automatically. Multi-level TLDs supported (e.g., co.uk, com.au)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="create_whois_server" class="block text-sm font-medium text-gray-700 mb-1">WHOIS Server (Optional)</label>
|
||||||
|
<input type="text" id="create_whois_server" name="whois_server"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="e.g., whois.verisign-grs.com">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="create_rdap_servers" class="block text-sm font-medium text-gray-700 mb-1">RDAP Servers (Optional)</label>
|
||||||
|
<textarea id="create_rdap_servers" name="rdap_servers" rows="2"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="e.g., https://rdap.verisign.com/com/v1/"></textarea>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">One URL per line or comma-separated</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="create_registry_url" class="block text-sm font-medium text-gray-700 mb-1">Registry URL (Optional)</label>
|
||||||
|
<input type="url" id="create_registry_url" name="registry_url"
|
||||||
|
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="e.g., https://www.verisign.com">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-6 py-4 bg-gray-50 flex justify-end space-x-3">
|
||||||
|
<button type="button" onclick="closeCreateTldModal()"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700">
|
||||||
|
Create TLD
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrf_token" value="<?= csrf_token() ?>">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Import TLD Modal -->
|
||||||
|
<div id="tldImportModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-md">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900">
|
||||||
|
<i class="fas fa-upload text-primary mr-2"></i>Import TLDs
|
||||||
|
</h3>
|
||||||
|
<button onclick="document.getElementById('tldImportModal').classList.add('hidden')" class="text-gray-400 hover:text-gray-600">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="/tld-registry/import" enctype="multipart/form-data" id="tldImportForm">
|
||||||
|
<?= csrf_field() ?>
|
||||||
|
<div class="p-6 space-y-4">
|
||||||
|
<!-- Drag & Drop Zone -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-700 mb-1.5">Select File</label>
|
||||||
|
<div id="tldDropzone" class="relative border-2 border-dashed border-gray-300 rounded-lg p-6 text-center cursor-pointer transition-all hover:border-primary hover:bg-gray-50">
|
||||||
|
<input type="file" name="import_file" accept=".csv,.json" required id="tldFileInput"
|
||||||
|
class="absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10">
|
||||||
|
<div id="tldDropzoneContent">
|
||||||
|
<i class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-2"></i>
|
||||||
|
<p class="text-sm text-gray-600 font-medium">Drag & drop your file here</p>
|
||||||
|
<p class="text-xs text-gray-400 my-1">or</p>
|
||||||
|
<span class="inline-flex items-center px-3 py-1.5 bg-primary text-white text-xs rounded-lg font-medium">
|
||||||
|
<i class="fas fa-folder-open mr-1.5"></i>Browse Files
|
||||||
|
</span>
|
||||||
|
<p class="mt-2.5 text-xs text-gray-400">CSV, JSON · Max <?= \App\Helpers\ViewHelper::getMaxUploadSize() ?></p>
|
||||||
|
</div>
|
||||||
|
<div id="tldDropzoneFile" class="hidden">
|
||||||
|
<i class="fas fa-file-alt text-2xl text-primary mb-1.5"></i>
|
||||||
|
<p class="text-sm font-medium text-gray-700" id="tldFileName"></p>
|
||||||
|
<p class="text-xs text-gray-400" id="tldFileSize"></p>
|
||||||
|
<button type="button" id="tldFileRemove" class="mt-1.5 text-xs text-red-500 hover:text-red-700 font-medium">
|
||||||
|
<i class="fas fa-trash-alt mr-1"></i>Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3">
|
||||||
|
<p class="text-xs text-gray-700 font-medium mb-1"><i class="fas fa-info-circle text-blue-500 mr-1"></i> Expected Format</p>
|
||||||
|
<p class="text-xs text-gray-600">CSV columns: <code class="bg-white px-1 rounded">tld, whois_server, rdap_servers, registry_url, is_active</code></p>
|
||||||
|
<p class="text-xs text-gray-600 mt-0.5">JSON: array of objects with same fields</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Existing TLDs will be updated. New TLDs will be created as active.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50 flex justify-end gap-2 rounded-b-lg">
|
||||||
|
<button type="button" onclick="document.getElementById('tldImportModal').classList.add('hidden')" class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button type="submit" id="tldImportBtn" class="px-4 py-2 bg-primary text-white rounded-lg text-sm font-medium hover:bg-primary-dark transition-colors">
|
||||||
|
<i class="fas fa-upload mr-1.5"></i>Import TLDs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
function toggleAllCheckboxes(selectAllCheckbox) {
|
function toggleAllCheckboxes(selectAllCheckbox) {
|
||||||
const checkboxes = document.querySelectorAll('.tld-checkbox');
|
const checkboxes = document.querySelectorAll('.tld-checkbox');
|
||||||
@@ -608,6 +762,130 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create TLD Modal
|
||||||
|
function openCreateTldModal() {
|
||||||
|
document.getElementById('createTldModal').classList.remove('hidden');
|
||||||
|
document.getElementById('create_tld').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCreateTldModal() {
|
||||||
|
document.getElementById('createTldModal').classList.add('hidden');
|
||||||
|
document.querySelector('#createTldModal form').reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('createTldModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
closeCreateTldModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import Modal
|
||||||
|
document.getElementById('tldImportModal').addEventListener('click', function(e) {
|
||||||
|
if (e.target === this) {
|
||||||
|
document.getElementById('tldImportModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close dropdowns when clicking outside
|
||||||
|
document.addEventListener('click', function(e) {
|
||||||
|
const exportWrapper = document.getElementById('tldExportDropdownWrapper');
|
||||||
|
if (exportWrapper && !exportWrapper.contains(e.target)) {
|
||||||
|
document.getElementById('tldExportDropdownMenu').classList.add('hidden');
|
||||||
|
}
|
||||||
|
const ianaWrapper = document.getElementById('ianaDropdownWrapper');
|
||||||
|
if (ianaWrapper && !ianaWrapper.contains(e.target)) {
|
||||||
|
document.getElementById('ianaDropdownMenu').classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modals on escape key
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeCreateTldModal();
|
||||||
|
document.getElementById('tldImportModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Import drag-and-drop & loading
|
||||||
|
(function() {
|
||||||
|
const dropzone = document.getElementById('tldDropzone');
|
||||||
|
const fileInput = document.getElementById('tldFileInput');
|
||||||
|
const content = document.getElementById('tldDropzoneContent');
|
||||||
|
const fileInfo = document.getElementById('tldDropzoneFile');
|
||||||
|
const fileName = document.getElementById('tldFileName');
|
||||||
|
const fileSize = document.getElementById('tldFileSize');
|
||||||
|
const removeBtn = document.getElementById('tldFileRemove');
|
||||||
|
const form = document.getElementById('tldImportForm');
|
||||||
|
const submitBtn = document.getElementById('tldImportBtn');
|
||||||
|
|
||||||
|
if (!dropzone) return;
|
||||||
|
|
||||||
|
function formatSize(bytes) {
|
||||||
|
if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB';
|
||||||
|
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||||
|
return bytes + ' B';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFile(file) {
|
||||||
|
fileName.textContent = file.name;
|
||||||
|
fileSize.textContent = formatSize(file.size);
|
||||||
|
content.classList.add('hidden');
|
||||||
|
fileInfo.classList.remove('hidden');
|
||||||
|
dropzone.classList.remove('border-gray-300');
|
||||||
|
dropzone.classList.add('border-primary', 'bg-primary/5');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetDropzone() {
|
||||||
|
fileInput.value = '';
|
||||||
|
content.classList.remove('hidden');
|
||||||
|
fileInfo.classList.add('hidden');
|
||||||
|
dropzone.classList.add('border-gray-300');
|
||||||
|
dropzone.classList.remove('border-primary', 'bg-primary/5');
|
||||||
|
}
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', function() {
|
||||||
|
if (this.files.length) showFile(this.files[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
removeBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
resetDropzone();
|
||||||
|
});
|
||||||
|
|
||||||
|
['dragenter', 'dragover'].forEach(evt => {
|
||||||
|
dropzone.addEventListener(evt, function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
dropzone.classList.add('border-primary', 'bg-primary/5');
|
||||||
|
dropzone.classList.remove('border-gray-300');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
['dragleave', 'drop'].forEach(evt => {
|
||||||
|
dropzone.addEventListener(evt, function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!fileInput.files.length) {
|
||||||
|
dropzone.classList.remove('border-primary', 'bg-primary/5');
|
||||||
|
dropzone.classList.add('border-gray-300');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dropzone.addEventListener('drop', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const files = e.dataTransfer.files;
|
||||||
|
if (files.length) {
|
||||||
|
fileInput.files = files;
|
||||||
|
showFile(files[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
form.addEventListener('submit', function() {
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1.5"></i>Importing...';
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ INSERT INTO settings (setting_key, setting_value, `type`, `description`) VALUES
|
|||||||
('app_name', 'Domain Monitor', 'string', 'Application name'),
|
('app_name', 'Domain Monitor', 'string', 'Application name'),
|
||||||
('app_url', 'http://localhost:8000', 'string', 'Application URL'),
|
('app_url', 'http://localhost:8000', 'string', 'Application URL'),
|
||||||
('app_timezone', 'UTC', 'string', 'Application timezone'),
|
('app_timezone', 'UTC', 'string', 'Application timezone'),
|
||||||
('app_version', '1.1.3', 'string', 'Application version number'),
|
('app_version', '1.1.4', 'string', 'Application version number'),
|
||||||
|
|
||||||
-- Email settings
|
-- Email settings
|
||||||
('mail_host', 'smtp.mailtrap.io', 'string', 'SMTP server host'),
|
('mail_host', 'smtp.mailtrap.io', 'string', 'SMTP server host'),
|
||||||
|
|||||||
7
database/migrations/026_update_app_version_v1.1.4.sql
Normal file
7
database/migrations/026_update_app_version_v1.1.4.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
-- Update application version to 1.1.4
|
||||||
|
-- This version adds TLD Registry import/export/create, IANA dropdown UI, and standardized logging
|
||||||
|
|
||||||
|
-- Update application version to 1.1.4
|
||||||
|
UPDATE settings
|
||||||
|
SET setting_value = '1.1.4'
|
||||||
|
WHERE setting_key = 'app_version';
|
||||||
@@ -108,6 +108,9 @@ $router->post('/channels/test', [NotificationGroupController::class, 'testChanne
|
|||||||
|
|
||||||
// TLD Registry
|
// TLD Registry
|
||||||
$router->get('/tld-registry', [TldRegistryController::class, 'index']);
|
$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->get('/tld-registry/{id}', [TldRegistryController::class, 'show']);
|
||||||
$router->post('/tld-registry/import-tld-list', [TldRegistryController::class, 'importTldList']);
|
$router->post('/tld-registry/import-tld-list', [TldRegistryController::class, 'importTldList']);
|
||||||
$router->post('/tld-registry/import-rdap', [TldRegistryController::class, 'importRdap']);
|
$router->post('/tld-registry/import-rdap', [TldRegistryController::class, 'importRdap']);
|
||||||
|
|||||||
Reference in New Issue
Block a user