Files
domnitor/app/Controllers/TldRegistryController.php
Hosteroid ed3e5739f4 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.
2026-03-02 11:17:58 +02:00

1167 lines
38 KiB
PHP

<?php
namespace App\Controllers;
use Core\Controller;
use Core\Auth;
use App\Models\TldRegistry;
use App\Models\TldImportLog;
use App\Services\TldRegistryService;
use App\Services\Logger;
class TldRegistryController extends Controller
{
private TldRegistry $tldModel;
private TldImportLog $importLogModel;
private TldRegistryService $tldService;
private Logger $logger;
public function __construct()
{
$this->tldModel = new TldRegistry();
$this->importLogModel = new TldImportLog();
$this->tldService = new TldRegistryService();
$this->logger = new Logger('tld_registry_controller');
}
/**
* Display TLD registry dashboard
*/
public function index()
{
$search = \App\Helpers\InputValidator::sanitizeSearch($_GET['search'] ?? '', 100);
$status = $_GET['status'] ?? '';
$dataType = $_GET['data_type'] ?? '';
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = max(10, min(100, (int)($_GET['per_page'] ?? 50)));
$sort = $_GET['sort'] ?? 'tld';
$order = $_GET['order'] ?? 'asc';
$result = $this->tldModel->getPaginated($page, $perPage, $search, $sort, $order, $status, $dataType);
$tldStats = $this->tldModel->getStatistics();
$this->view('tld-registry/index', [
'tlds' => $result['tlds'],
'pagination' => $result['pagination'],
'tldStats' => $tldStats,
'filters' => [
'search' => $search,
'status' => $status,
'data_type' => $dataType,
'sort' => $sort,
'order' => $order
],
'title' => 'TLD Registry'
]);
}
/**
* Show TLD details
*/
public function show($params = [])
{
$id = $params['id'] ?? 0;
$tld = $this->tldModel->find($id);
if (!$tld) {
$_SESSION['error'] = 'TLD not found';
$this->redirect('/tld-registry');
return;
}
$this->view('tld-registry/view', [
'tld' => $tld,
'title' => 'TLD: ' . $tld['tld']
]);
}
/**
* Import TLD list from IANA
*/
public function importTldList()
{
Auth::requireAdmin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->redirect('/tld-registry');
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
try {
$stats = $this->tldService->importTldList();
$message = "TLD list import completed: ";
$message .= "{$stats['total_tlds']} total, ";
$message .= "{$stats['new_tlds']} new, ";
$message .= "{$stats['updated_tlds']} updated";
if ($stats['failed_tlds'] > 0) {
$message .= ", {$stats['failed_tlds']} failed";
}
$message .= ". Next: Import RDAP servers for these TLDs.";
$_SESSION['success'] = $message;
} catch (\Exception $e) {
$_SESSION['error'] = 'TLD list import failed: ' . $e->getMessage();
}
$this->redirect('/tld-registry');
}
/**
* Import RDAP data from IANA
*/
public function importRdap()
{
Auth::requireAdmin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->redirect('/tld-registry');
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
try {
$stats = $this->tldService->importRdapData();
$message = "RDAP import completed: ";
$message .= "{$stats['total_tlds']} total, ";
$message .= "{$stats['new_tlds']} new, ";
$message .= "{$stats['updated_tlds']} updated";
if ($stats['failed_tlds'] > 0) {
$message .= ", {$stats['failed_tlds']} failed";
}
$message .= ". Next: Import WHOIS servers for TLDs missing RDAP.";
$_SESSION['success'] = $message;
} catch (\Exception $e) {
$_SESSION['error'] = 'RDAP import failed: ' . $e->getMessage();
}
$this->redirect('/tld-registry');
}
/**
* Import WHOIS data for missing TLDs
*/
public function importWhois()
{
Auth::requireAdmin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->redirect('/tld-registry');
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
try {
$stats = $this->tldService->importWhoisDataForMissingTlds();
$remainingCount = $this->tldService->getTldsNeedingWhoisCount();
$message = "WHOIS import completed: ";
$message .= "{$stats['total_tlds']} total, ";
$message .= "{$stats['updated_tlds']} updated";
if ($stats['failed_tlds'] > 0) {
$message .= ", {$stats['failed_tlds']} failed";
}
if ($remainingCount > 0) {
$message .= ". {$remainingCount} TLDs still need WHOIS data. Run import again to continue.";
} else {
$message .= ". TLD registry setup complete! Use 'Check Updates' to monitor for changes.";
}
$_SESSION['success'] = $message;
} catch (\Exception $e) {
$_SESSION['error'] = 'WHOIS import failed: ' . $e->getMessage();
}
$this->redirect('/tld-registry');
}
/**
* Check for IANA updates
*/
public function checkUpdates()
{
Auth::requireAdmin();
try {
$updateInfo = $this->tldService->checkForUpdates();
if ($updateInfo['overall_needs_update']) {
$messages = [];
if ($updateInfo['tld_list']['needs_update']) {
$messages[] = "TLD list updated: Version " .
($updateInfo['tld_list']['current_version'] ?? 'Unknown') .
" (was " . ($updateInfo['tld_list']['last_version'] ?? 'None') . ")";
}
if ($updateInfo['rdap']['needs_update']) {
$messages[] = "RDAP data updated: " .
($updateInfo['rdap']['current_publication'] ?? 'Unknown') .
" (was " . ($updateInfo['rdap']['last_publication'] ?? 'None') . ")";
}
$_SESSION['info'] = "IANA data has been updated. " . implode(' | ', $messages);
} else {
$_SESSION['success'] = "TLD registry is up to date";
}
// Show any errors
if (!empty($updateInfo['errors'])) {
$_SESSION['warning'] = "Some checks failed: " . implode(', ', $updateInfo['errors']);
}
} catch (\Exception $e) {
$_SESSION['error'] = 'Failed to check for updates: ' . $e->getMessage();
}
$this->redirect('/tld-registry');
}
/**
* Start progressive import (universal)
*/
public function startProgressiveImport()
{
Auth::requireAdmin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->redirect('/tld-registry');
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
$importType = $_POST['import_type'] ?? '';
$this->logger->separator('Start Progressive Import');
$this->logger->info('Import requested', [
'type' => $importType,
'user_id' => $_SESSION['user_id'] ?? 'unknown',
'username' => $_SESSION['username'] ?? 'unknown'
]);
if (!in_array($importType, ['tld_list', 'rdap', 'whois', 'check_updates', 'complete_workflow'])) {
$this->logger->warning('Invalid import type provided', ['type' => $importType]);
$_SESSION['error'] = 'Invalid import type';
$this->redirect('/tld-registry');
return;
}
try {
$result = $this->tldService->startProgressiveImport($importType);
$this->logger->info('Import started', [
'status' => $result['status'],
'log_id' => $result['log_id'] ?? null,
'message' => $result['message'] ?? ''
]);
if ($result['status'] === 'complete') {
$_SESSION['success'] = $result['message'];
$this->redirect('/tld-registry');
} else {
// Redirect to progress page
$this->logger->info('Redirecting to progress page', ['log_id' => $result['log_id']]);
$this->redirect('/tld-registry/import-progress/' . $result['log_id']);
}
} catch (\Exception $e) {
$this->logger->error('Failed to start import', [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
$_SESSION['error'] = 'Failed to start import: ' . $e->getMessage();
$this->redirect('/tld-registry');
}
}
/**
* Show import progress page (universal)
*/
public function importProgress($params = [])
{
$logId = $params['log_id'] ?? 0;
$this->logger->info('Import progress page requested', ['log_id' => $logId]);
if (!$logId) {
$this->logger->warning('Progress page requested with no log_id');
$_SESSION['error'] = 'Invalid import session';
$this->redirect('/tld-registry');
return;
}
// Get import type from log
$log = $this->importLogModel->find($logId);
if (!$log) {
$this->logger->error('Import log not found', ['log_id' => $logId]);
$_SESSION['error'] = 'Import log not found';
$this->redirect('/tld-registry');
return;
}
$importType = $log['import_type'];
$this->logger->info('Showing progress page', [
'log_id' => $logId,
'import_type' => $importType,
'status' => $log['status']
]);
$titles = [
'tld_list' => 'TLD List Import Progress',
'rdap' => 'RDAP Import Progress',
'whois' => 'WHOIS Import Progress',
'check_updates' => 'Update Check Progress',
'complete_workflow' => 'Complete TLD Import Workflow'
];
$this->view('tld-registry/import-progress', [
'log_id' => $logId,
'import_type' => $importType,
'title' => $titles[$importType] ?? 'Import Progress'
]);
}
/**
* API endpoint to get import progress
*/
public function apiGetImportProgress()
{
// Start detailed logging
$this->logger->separator('API Import Progress Request');
$this->logger->info('API called', [
'log_id' => $_GET['log_id'] ?? 'none',
'user_id' => $_SESSION['user_id'] ?? 'not set',
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
]);
// Start output buffering to catch any accidental output
ob_start();
try {
// Clear any previous output
ob_clean();
// Set JSON header immediately
header('Content-Type: application/json');
$logId = $_GET['log_id'] ?? 0;
if (!$logId) {
$this->logger->warning('API call with missing log_id');
ob_end_clean();
$this->json(['error' => 'Log ID required'], 400);
return;
}
// Ensure user is authenticated
if (!isset($_SESSION['user_id'])) {
$this->logger->warning('Unauthenticated API call attempt', ['log_id' => $logId]);
ob_end_clean();
$this->json(['error' => 'Authentication required'], 401);
return;
}
$this->logger->info('Processing batch', ['log_id' => $logId]);
// Add detailed logging around the service call
$this->logger->info('About to call TldRegistryService::processNextBatch', [
'log_id' => $logId,
'service_class' => get_class($this->tldService)
]);
try {
$result = $this->tldService->processNextBatch($logId);
$this->logger->info('processNextBatch returned successfully');
} catch (\Throwable $e) {
$this->logger->critical('processNextBatch threw exception', [
'type' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
throw $e;
}
$this->logger->info('Batch processing result', [
'status' => $result['status'] ?? 'unknown',
'processed' => $result['processed'] ?? 0,
'remaining' => $result['remaining'] ?? 0
]);
// Clean output buffer before sending JSON
ob_end_clean();
$this->json($result);
} catch (\Throwable $e) {
// Catch ALL errors including fatal errors
// Clean any buffered output
if (ob_get_level() > 0) {
ob_end_clean();
}
// Detailed error logging
$this->logger->critical('API Import Progress Fatal Error', [
'type' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'log_id' => $_GET['log_id'] ?? 'none'
]);
$this->logger->error('Stack trace', ['trace' => $e->getTraceAsString()]);
$this->logger->separator('API Error End');
// Ensure we always send JSON
if (!headers_sent()) {
http_response_code(500);
header('Content-Type: application/json');
}
echo json_encode([
'error' => 'An error occurred while processing the import',
'message' => $e->getMessage(),
'status' => 'error',
'log_id' => $_GET['log_id'] ?? null,
'error_type' => get_class($e)
]);
exit;
}
}
/**
* Bulk delete TLDs
*/
public function bulkDelete()
{
Auth::requireAdmin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->redirect('/tld-registry');
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
$tldIds = $_POST['tld_ids'] ?? [];
if (empty($tldIds)) {
$_SESSION['error'] = 'No TLDs selected for deletion';
$this->redirect('/tld-registry');
return;
}
// Validate bulk operation size
$sizeError = \App\Helpers\InputValidator::validateArraySize($tldIds, 500, 'TLD selection');
if ($sizeError) {
$_SESSION['error'] = $sizeError;
$this->redirect('/tld-registry');
return;
}
try {
$deletedCount = 0;
foreach ($tldIds as $id) {
if ($this->tldModel->delete($id)) {
$deletedCount++;
}
}
$_SESSION['success'] = "Successfully deleted {$deletedCount} TLD(s)";
} catch (\Exception $e) {
$_SESSION['error'] = 'Failed to delete TLDs: ' . $e->getMessage();
}
$this->redirect('/tld-registry');
}
/**
* Toggle TLD active status
*/
public function toggleActive($params = [])
{
Auth::requireAdmin();
$id = $params['id'] ?? 0;
$tld = $this->tldModel->find($id);
if (!$tld) {
$_SESSION['error'] = 'TLD not found';
$this->redirect('/tld-registry');
return;
}
$this->tldModel->toggleActive($id);
$status = $tld['is_active'] ? 'disabled' : 'enabled';
$_SESSION['success'] = "TLD {$tld['tld']} has been {$status}";
// Redirect back to the same page (either view page or index page)
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if (strpos($referer, '/tld-registry/' . $id) !== false) {
// Came from view page, go back to view page
$this->redirect('/tld-registry/' . $id);
} else {
// Came from index page or elsewhere, go to index
$this->redirect('/tld-registry');
}
}
/**
* Refresh TLD data from IANA
*/
public function refresh($params = [])
{
Auth::requireAdmin();
$id = $params['id'] ?? 0;
$tld = $this->tldModel->find($id);
if (!$tld) {
$_SESSION['error'] = 'TLD not found';
$this->redirect('/tld-registry');
return;
}
try {
// Remove dot from TLD for URL
$tldForUrl = ltrim($tld['tld'], '.');
$url = "https://www.iana.org/domains/root/db/{$tldForUrl}.html";
$client = new \GuzzleHttp\Client();
$response = $client->get($url);
$html = $response->getBody()->getContents();
// Extract data from HTML
$whoisServer = $this->extractWhoisServer($html);
$lastUpdated = $this->extractLastUpdated($html);
$registryUrl = $this->extractRegistryUrl($html);
$registrationDate = $this->extractRegistrationDate($html);
$updateData = [
'updated_at' => date('Y-m-d H:i:s')
];
if ($whoisServer) $updateData['whois_server'] = $whoisServer;
if ($lastUpdated) $updateData['record_last_updated'] = $lastUpdated;
if ($registryUrl) $updateData['registry_url'] = $registryUrl;
if ($registrationDate) $updateData['registration_date'] = $registrationDate;
$this->tldModel->update($id, $updateData);
$_SESSION['success'] = "TLD {$tld['tld']} data refreshed successfully";
} catch (\Exception $e) {
$_SESSION['error'] = 'Failed to refresh TLD data: ' . $e->getMessage();
}
// Redirect back to the same page (either view page or index page)
$referer = $_SERVER['HTTP_REFERER'] ?? '';
if (strpos($referer, '/tld-registry/' . $id) !== false) {
// Came from view page, go back to view page
$this->redirect('/tld-registry/' . $id);
} else {
// Came from index page or elsewhere, go to index
$this->redirect('/tld-registry');
}
}
/**
* Show import logs
*/
public function importLogs()
{
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = max(10, min(100, (int)($_GET['per_page'] ?? 20)));
$result = $this->importLogModel->getPaginated($page, $perPage);
$importStats = $this->importLogModel->getImportStatistics();
$this->view('tld-registry/import-logs', [
'imports' => $result['logs'],
'pagination' => $result['pagination'],
'importStats' => $importStats,
'title' => 'TLD Import Logs'
]);
}
/**
* API endpoint to get TLD info for a domain
*/
public function apiGetTldInfo()
{
$domain = $_GET['domain'] ?? '';
if (empty($domain)) {
$this->json(['error' => 'Domain parameter is required'], 400);
return;
}
try {
$tldInfo = $this->tldService->getTldInfo($domain);
if ($tldInfo) {
$this->json([
'success' => true,
'data' => $tldInfo
]);
} else {
$this->json([
'success' => false,
'message' => 'TLD information not found'
]);
}
} catch (\Exception $e) {
$this->json([
'success' => false,
'error' => $e->getMessage()
], 500);
}
}
/**
* 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
*/
private function extractWhoisServer(string $html): ?string
{
if (preg_match('/WHOIS Server:\s*([^\s<]+)/i', $html, $matches)) {
return trim($matches[1]);
}
return null;
}
/**
* Extract last updated date from HTML
*/
private function extractLastUpdated(string $html): ?string
{
if (preg_match('/Record last updated\s+(\d{4}-\d{2}-\d{2})/i', $html, $matches)) {
return $matches[1] . ' 00:00:00';
}
return null;
}
/**
* Extract registry URL from HTML
*/
private function extractRegistryUrl(string $html): ?string
{
if (preg_match('/URL for registration services:\s*([^\s<]+)/i', $html, $matches)) {
return trim($matches[1]);
}
return null;
}
/**
* Extract registration date from HTML
*/
private function extractRegistrationDate(string $html): ?string
{
if (preg_match('/Registration date\s+(\d{4}-\d{2}-\d{2})/i', $html, $matches)) {
return $matches[1];
}
return null;
}
/**
* Update WHOIS server for a TLD
*/
public function updateWhoisServer($params = [])
{
Auth::requireAdmin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->redirect('/tld-registry');
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
$id = $params['id'] ?? 0;
$tld = $this->tldModel->find($id);
if (!$tld) {
$_SESSION['error'] = 'TLD not found';
$this->redirect('/tld-registry');
return;
}
$whoisServer = trim($_POST['whois_server'] ?? '');
// Validate WHOIS server format (basic validation)
if (!empty($whoisServer) && !preg_match('/^[a-z0-9.-]+\.[a-z]{2,}$/i', $whoisServer)) {
$_SESSION['error'] = 'Invalid WHOIS server format';
$this->redirect('/tld-registry/' . $id);
return;
}
try {
$this->tldModel->update($id, [
'whois_server' => !empty($whoisServer) ? $whoisServer : null,
'updated_at' => date('Y-m-d H:i:s')
]);
$this->logger->info('WHOIS server updated', [
'tld_id' => $id,
'tld' => $tld['tld'],
'whois_server' => $whoisServer,
'user_id' => $_SESSION['user_id'] ?? 'unknown'
]);
$_SESSION['success'] = "WHOIS server updated successfully for {$tld['tld']}";
} catch (\Exception $e) {
$_SESSION['error'] = 'Failed to update WHOIS server: ' . $e->getMessage();
$this->logger->error('Failed to update WHOIS server', [
'tld_id' => $id,
'error' => $e->getMessage()
]);
}
$this->redirect('/tld-registry/' . $id);
}
/**
* Update RDAP servers for a TLD
*/
public function updateRdapServers($params = [])
{
Auth::requireAdmin();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->redirect('/tld-registry');
return;
}
// CSRF Protection
$this->verifyCsrf('/tld-registry');
$id = $params['id'] ?? 0;
$tld = $this->tldModel->find($id);
if (!$tld) {
$_SESSION['error'] = 'TLD not found';
$this->redirect('/tld-registry');
return;
}
$rdapServersInput = trim($_POST['rdap_servers'] ?? '');
$rdapServers = [];
// Parse RDAP servers (comma or newline separated)
if (!empty($rdapServersInput)) {
$servers = preg_split('/[,\n\r]+/', $rdapServersInput);
foreach ($servers as $server) {
$server = trim($server);
if (!empty($server)) {
// Basic URL validation
if (filter_var($server, FILTER_VALIDATE_URL) ||
preg_match('/^https?:\/\/[a-z0-9.-]+\.[a-z]{2,}(\/.*)?$/i', $server)) {
// Ensure URL ends with / if not already
$server = rtrim($server, '/') . '/';
$rdapServers[] = $server;
}
}
}
}
try {
$this->tldModel->update($id, [
'rdap_servers' => !empty($rdapServers) ? json_encode($rdapServers) : null,
'updated_at' => date('Y-m-d H:i:s')
]);
$this->logger->info('RDAP servers updated', [
'tld_id' => $id,
'tld' => $tld['tld'],
'rdap_servers_count' => count($rdapServers),
'user_id' => $_SESSION['user_id'] ?? 'unknown'
]);
$_SESSION['success'] = "RDAP servers updated successfully for {$tld['tld']} (" . count($rdapServers) . " server(s))";
} catch (\Exception $e) {
$_SESSION['error'] = 'Failed to update RDAP servers: ' . $e->getMessage();
$this->logger->error('Failed to update RDAP servers', [
'tld_id' => $id,
'error' => $e->getMessage()
]);
}
$this->redirect('/tld-registry/' . $id);
}
}