diff --git a/cp/app/Controllers/SystemController.php b/cp/app/Controllers/SystemController.php index b9786f8..a6028f7 100644 --- a/cp/app/Controllers/SystemController.php +++ b/cp/app/Controllers/SystemController.php @@ -983,7 +983,7 @@ class SystemController extends Controller // Mapping of regex patterns to script names $regexToScriptName = [ '/^(?!-)(?!.*--)[A-Z0-9-]{1,63}(? 'ASCII', - '/^[а-яА-ЯґҐєЄіІїЇѝЍћЋљЈ]+$/u' => 'Cyrillic', + '/^[а-яА-ЯґҐєЄіІїЇѝЍћЋљЈ0-9ʼѫѣѭ]+$/u' => 'Cyrillic', '/^[ぁ-んァ-ン一-龯々]+$/u' => 'Japanese', '/^[가-힣]+$/u' => 'Korean', '/^(?!-)(?!.*--)[\x{0621}-\x{064A}\x{0660}-\x{0669}\x{0671}-\x{06D3}-]{1,63}(? 'Arabic', @@ -1463,4 +1463,151 @@ class SystemController extends Controller } + public function idnexport(Request $request, Response $response, $args) + { + if ($_SESSION["auth_roles"] != 0) { + return $response->withHeader('Location', '/dashboard')->withStatus(302); + } + + $db = $this->container->get('db'); + + if ($args) { + $args = trim($args); + + if (!preg_match('/^\.(xn--[a-zA-Z0-9-]+|[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)?)$/', $args)) { + $this->container->get('flash')->addMessage('error', 'Invalid TLD format'); + return $response->withHeader('Location', '/registry/tlds')->withStatus(302); + } + + $idn_table = $db->selectValue('SELECT idn_table FROM domain_tld WHERE tld = ?', + [ $args ]); + + $idn_table_map = [ + '/^(?!-)(?!.*--)[A-Z0-9-]{1,63}(? 'ascii', + '/^[а-яА-ЯґҐєЄіІїЇѝЍћЋљЈ0-9ʼѫѣѭ]+$/u' => 'cyrillic', + '/^[ぁ-んァ-ン一-龯々0-9]+$/u' => 'japanese', + '/^[가-힣0-9]+$/u' => 'korean', + '/^(?!-)(?!.*--)[\x{0621}-\x{064A}\x{0660}-\x{0669}\x{0671}-\x{06D3}-]{1,63}(? 'arabic' + ]; + + $idn_table_name = 'ascii'; // Default + + foreach ($idn_table_map as $regex => $name) { + if ($idn_table === $regex) { + $idn_table_name = $name; + break; + } + } + + $company_name = $db->selectValue("SELECT value FROM settings WHERE name = 'company_name'"); + $address = $db->selectValue("SELECT value FROM settings WHERE name = 'address'"); + $address2 = $db->selectValue("SELECT value FROM settings WHERE name = 'address2'"); + $cc = $db->selectValue("SELECT value FROM settings WHERE name = 'cc'"); + $phone = $db->selectValue("SELECT value FROM settings WHERE name = 'phone'"); + $email = $db->selectValue("SELECT value FROM settings WHERE name = 'email'"); + + // Set the regex and metadata based on the script. + switch ($idn_table_name) { + case 'ascii': + $idntable = '/^(?!-)(?!.*--)[A-Z0-9-]{1,63}(? $company_name, + 'Script' => 'ASCII', + 'Version' => '1.0', + 'Effective Date'=> date('Y-m-d'), + 'Contact' => $email, + 'Address' => $address . ', ' . $address2 . ', ' . $cc, + 'Telephone' => $phone, + 'Website' => 'www.example.com', + 'Notes' => 'This table describes codepoints allowed for the ASCII script.' + ]; + break; + case 'cyrillic': + $idntable = '/^[а-яА-ЯґҐєЄіІїЇѝЍћЋљЈ0-9ʼѫѣѭ]+$/u'; + $metadata = [ + 'Registry' => $company_name, + 'Script' => 'Cyrillic', + 'Version' => '1.0', + 'Effective Date'=> date('Y-m-d'), + 'Contact' => $email, + 'Address' => $address . ', ' . $address2 . ', ' . $cc, + 'Telephone' => $phone, + 'Website' => 'www.example.com', + 'Notes' => 'This table describes codepoints allowed for the Cyrillic script.' + ]; + break; + case 'japanese': + $idntable = '/^[ぁ-んァ-ン一-龯々0-9]+$/u'; + $metadata = [ + 'Registry' => $company_name, + 'Script' => 'Japanese', + 'Version' => '1.0', + 'Effective Date'=> date('Y-m-d'), + 'Contact' => $email, + 'Address' => $address . ', ' . $address2 . ', ' . $cc, + 'Telephone' => $phone, + 'Website' => 'www.example.com', + 'Notes' => 'This table describes codepoints allowed for the Japanese script.' + ]; + break; + case 'korean': + $idntable = '/^[가-힣0-9]+$/u'; + $metadata = [ + 'Registry' => $company_name, + 'Script' => 'Korean', + 'Version' => '1.0', + 'Effective Date'=> date('Y-m-d'), + 'Contact' => $email, + 'Address' => $address . ', ' . $address2 . ', ' . $cc, + 'Telephone' => $phone, + 'Website' => 'www.example.com', + 'Notes' => 'This table describes codepoints allowed for the Korean script.' + ]; + break; + case 'arabic': + $idntable = '/^(?!-)(?!.*--)[\x{0621}-\x{064A}\x{0660}-\x{0669}\x{0671}-\x{06D3}-]{1,63}(? $company_name, + 'Script' => 'Arabic', + 'Version' => '1.0', + 'Effective Date'=> date('Y-m-d'), + 'Contact' => $email, + 'Address' => $address . ', ' . $address2 . ', ' . $cc, + 'Telephone' => $phone, + 'Website' => 'www.example.com', + 'Notes' => 'This table describes codepoints allowed for the Arabic script.' + ]; + break; + default: + $idntable = '/^(?!-)(?!.*--)[A-Z0-9-]{1,63}(? $company_name, + 'Script' => 'ASCII (default)', + 'Version' => '1.0', + 'Effective Date'=> date('Y-m-d'), + 'Contact' => $email, + 'Address' => $address . ', ' . $address2 . ', ' . $cc, + 'Telephone' => $phone, + 'Website' => 'www.example.com', + 'Notes' => 'This table describes codepoints allowed for the ASCII script (default).' + ]; + break; + } + + try { + $table = generateIanaIdnTable($idntable, $metadata); + $response->getBody()->write($table); + return $response->withHeader('Content-Type', 'text/plain'); + } catch (Exception $e) { + $this->container->get('flash')->addMessage('error', 'Error generating table: ' . $e->getMessage()); + return $response->withHeader('Location', '/registry/tld/'.$args)->withStatus(302); + } + + } else { + // Redirect to the tlds view + return $response->withHeader('Location', '/registry/tlds')->withStatus(302); + } + + } + } \ No newline at end of file diff --git a/cp/bootstrap/helper.php b/cp/bootstrap/helper.php index 18d52e4..5705e21 100644 --- a/cp/bootstrap/helper.php +++ b/cp/bootstrap/helper.php @@ -888,3 +888,197 @@ function lacksRoles(int $userRoles, int ...$excludedRoles): bool { function hasOnlyRole(int $userRoles, int $specificRole): bool { return $userRoles === $specificRole; } + +// Returns an array of ranges: each item is ['start' => int, 'end' => int] +function parseCharacterClass(string $class): array { + $ranges = []; + $len = mb_strlen($class, 'UTF-8'); + $i = 0; + while ($i < $len) { + $currentCode = null; + // Look for an escape sequence like \x{0621} + if (mb_substr($class, $i, 1, 'UTF-8') === '\\') { + if (mb_substr($class, $i, 3, 'UTF-8') === '\\x{') { + $closePos = mb_strpos($class, '}', $i); + if ($closePos === false) { + throw new \RuntimeException("Unterminated escape sequence in character class."); + } + $hex = mb_substr($class, $i + 3, $closePos - ($i + 3), 'UTF-8'); + $currentCode = hexdec($hex); + $i = $closePos + 1; + } else { + // For a simple escaped char (for example, \-) + $i++; + if ($i < $len) { + $char = mb_substr($class, $i, 1, 'UTF-8'); + $currentCode = IntlChar::ord($char); + $i++; + } else { + break; + } + } + } else { + $char = mb_substr($class, $i, 1, 'UTF-8'); + $currentCode = IntlChar::ord($char); + $i++; + } + // Check if a dash follows and there is a token after it (forming a range) + if ($i < $len && mb_substr($class, $i, 1, 'UTF-8') === '-' && ($i + 1) < $len) { + // skip the dash + $i++; + $nextCode = null; + if (mb_substr($class, $i, 1, 'UTF-8') === '\\') { + if (mb_substr($class, $i, 3, 'UTF-8') === '\\x{') { + $closePos = mb_strpos($class, '}', $i); + if ($closePos === false) { + throw new \RuntimeException("Unterminated escape sequence in character class."); + } + $hex = mb_substr($class, $i + 3, $closePos - ($i + 3), 'UTF-8'); + $nextCode = hexdec($hex); + $i = $closePos + 1; + } else { + $i++; + if ($i < $len) { + $char = mb_substr($class, $i, 1, 'UTF-8'); + $nextCode = IntlChar::ord($char); + $i++; + } else { + break; + } + } + } else { + $char = mb_substr($class, $i, 1, 'UTF-8'); + $nextCode = IntlChar::ord($char); + $i++; + } + $ranges[] = [ + 'start' => min($currentCode, $nextCode), + 'end' => max($currentCode, $nextCode) + ]; + } else { + // Not a range; add the single codepoint. + $ranges[] = ['start' => $currentCode, 'end' => $currentCode]; + } + } + return $ranges; +} + +// --- Helper: merge overlapping ranges (optional) --- +function mergeRanges(array $ranges): array { + if (empty($ranges)) { + return []; + } + // sort ranges by start value + usort($ranges, fn($a, $b) => $a['start'] <=> $b['start']); + $merged = []; + $current = $ranges[0]; + foreach ($ranges as $r) { + if ($r['start'] <= $current['end'] + 1) { + // Extend the current range if overlapping or adjacent. + $current['end'] = max($current['end'], $r['end']); + } else { + $merged[] = $current; + $current = $r; + } + } + $merged[] = $current; + return $merged; +} + +// --- Helper: get Unicode name (or fallback) --- +function getUnicodeName(int $codepoint): string { + $name = IntlChar::charName($codepoint); + return $name !== '' ? $name : 'UNKNOWN'; +} + +// --- Main function: generate IANA IDN table from regex and metadata --- +function generateIanaIdnTable(string $regex, array $metadata): string { + $output = ''; + + // Extract modifier flags (e.g. 'i', 'u') from the regex delimiter. + if (!preg_match('/^.(.*).([a-zA-Z]*)$/', $regex, $parts)) { + throw new \RuntimeException("Regex does not have expected delimiter format."); + } + $patternBody = $parts[1]; + $modifiers = $parts[2]; + $caseInsensitive = strpos($modifiers, 'i') !== false; + + // Find all bracketed character classes. + if (!preg_match_all('/\[([^]]+)\]/u', $patternBody, $matches)) { + throw new \RuntimeException("No character classes found in regex."); + } + // Combine all character class contents. + $combinedClass = implode('', $matches[1]); + + // Parse the combined character class into ranges. + $ranges = parseCharacterClass($combinedClass); + + // If the regex is case‐insensitive, then for any range that covers A–Z add the lowercase equivalent. + if ($caseInsensitive) { + $additional = []; + foreach ($ranges as $range) { + // Check for Latin uppercase letters: U+0041 ('A') to U+005A ('Z') + if ($range['start'] >= 0x41 && $range['end'] <= 0x5A) { + $additional[] = [ + 'start' => $range['start'] + 0x20, + 'end' => $range['end'] + 0x20 + ]; + } + } + $ranges = array_merge($ranges, $additional); + } + // Optionally merge overlapping ranges. + $ranges = mergeRanges($ranges); + + // Build full list of allowed script-specific codepoints. + $scriptCodepoints = []; + foreach ($ranges as $range) { + for ($cp = $range['start']; $cp <= $range['end']; $cp++) { + $scriptCodepoints[$cp] = $cp; + } + } + ksort($scriptCodepoints); + + // Define the “common” codepoints (always allowed in all scripts) + $commonCodepoints = array_merge([0x002D], range(0x0030, 0x0039)); + // Remove common codepoints from script-specific set. + foreach ($commonCodepoints as $common) { + if (isset($scriptCodepoints[$common])) { + unset($scriptCodepoints[$common]); + } + } + + // Force all script-specific codepoints to lowercase and remove duplicates. + $lowerScriptCodepoints = []; + foreach ($scriptCodepoints as $cp) { + // Convert the codepoint to lowercase. + // For non-alphabetic characters, tolower() returns the original. + $lowerCp = IntlChar::tolower($cp); + $lowerScriptCodepoints[$lowerCp] = $lowerCp; + } + $scriptCodepoints = $lowerScriptCodepoints; + ksort($scriptCodepoints); + + // Build header block from metadata. + foreach ($metadata as $field => $value) { + $output .= "# {$field}: {$value}\n"; + } + $output .= "\n"; + + // Output the common codepoints. + $output .= "# Common (allowed in all scripts)\n"; + foreach ($commonCodepoints as $cp) { + $hex = sprintf("U+%04X", $cp); + $name = getUnicodeName($cp); + $output .= "{$hex} # {$name}\n"; + } + $output .= "\n"; + + // Output the script‐specific codepoints. + foreach ($scriptCodepoints as $cp) { + $hex = sprintf("U+%04X", $cp); + $name = getUnicodeName($cp); + $output .= "{$hex} # {$name}\n"; + } + return $output; +} \ No newline at end of file diff --git a/cp/resources/views/admin/registrars/index.twig b/cp/resources/views/admin/registrars/index.twig index e54ffaa..9b5f0b5 100644 --- a/cp/resources/views/admin/registrars/index.twig +++ b/cp/resources/views/admin/registrars/index.twig @@ -22,9 +22,9 @@
- Create Registrar + {{ __('Create Registrar') }} - +
diff --git a/cp/resources/views/admin/system/manageTld.twig b/cp/resources/views/admin/system/manageTld.twig index 10fdcad..7d32aa0 100644 --- a/cp/resources/views/admin/system/manageTld.twig +++ b/cp/resources/views/admin/system/manageTld.twig @@ -17,6 +17,18 @@ {{ __('Manage TLD') }} {{ tld_u }} + +
+
+ + + {{ __('Export IDN Table') }} + + + + +
+
diff --git a/cp/routes/web.php b/cp/routes/web.php index dd1bd13..61cbe44 100644 --- a/cp/routes/web.php +++ b/cp/routes/web.php @@ -141,6 +141,7 @@ $app->group('', function ($route) { $route->map(['GET', 'POST'], '/registry/tokens', SystemController::class .':manageTokens')->setName('manageTokens'); $route->post('/registry/promotions', SystemController::class . ':managePromo')->setName('managePromo'); $route->post('/registry/phases', SystemController::class . ':managePhases')->setName('managePhases'); + $route->get('/registry/idnexport/{script}', SystemController::class .':idnexport')->setName('idnexport'); $route->get('/support', SupportController::class .':view')->setName('ticketview'); $route->map(['GET', 'POST'], '/support/new', SupportController::class .':newticket')->setName('newticket');