Added ability to export IDN tables, fixed #193

This commit is contained in:
Pinga 2025-02-19 12:16:24 +02:00
parent 89ac087a3d
commit 955dd6ffe0
5 changed files with 357 additions and 3 deletions

View file

@ -983,7 +983,7 @@ class SystemController extends Controller
// Mapping of regex patterns to script names // Mapping of regex patterns to script names
$regexToScriptName = [ $regexToScriptName = [
'/^(?!-)(?!.*--)[A-Z0-9-]{1,63}(?<!-)(.(?!-)(?!.*--)[A-Z0-9-]{1,63}(?<!-))*$/i' => 'ASCII', '/^(?!-)(?!.*--)[A-Z0-9-]{1,63}(?<!-)(.(?!-)(?!.*--)[A-Z0-9-]{1,63}(?<!-))*$/i' => 'ASCII',
'/^[а-яА-ЯґҐєЄіІїЇѝЍћЋљЈ]+$/u' => 'Cyrillic', '/^[а-яА-ЯґҐєЄіІїЇѝЍћЋљЈ0-9ʼѫѣѭ]+$/u' => 'Cyrillic',
'/^[ぁ-んァ-ン一-龯々]+$/u' => 'Japanese', '/^[ぁ-んァ-ン一-龯々]+$/u' => 'Japanese',
'/^[가-힣]+$/u' => 'Korean', '/^[가-힣]+$/u' => 'Korean',
'/^(?!-)(?!.*--)[\x{0621}-\x{064A}\x{0660}-\x{0669}\x{0671}-\x{06D3}-]{1,63}(?<!-)$/u' => 'Arabic', '/^(?!-)(?!.*--)[\x{0621}-\x{064A}\x{0660}-\x{0669}\x{0671}-\x{06D3}-]{1,63}(?<!-)$/u' => '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}(?<!-)(.(?!-)(?!.*--)[A-Z0-9-]{1,63}(?<!-))*$/i' => '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}(?<!-)$/u' => '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}(?<!-)(.(?!-)(?!.*--)[A-Z0-9-]{1,63}(?<!-))*$/i';
$metadata = [
'Registry' => $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}(?<!-)$/u';
$metadata = [
'Registry' => $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}(?<!-)(.(?!-)(?!.*--)[A-Z0-9-]{1,63}(?<!-))*$/i';
$metadata = [
'Registry' => $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);
}
}
} }

View file

@ -888,3 +888,197 @@ function lacksRoles(int $userRoles, int ...$excludedRoles): bool {
function hasOnlyRole(int $userRoles, int $specificRole): bool { function hasOnlyRole(int $userRoles, int $specificRole): bool {
return $userRoles === $specificRole; 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 caseinsensitive, then for any range that covers AZ 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 scriptspecific codepoints.
foreach ($scriptCodepoints as $cp) {
$hex = sprintf("U+%04X", $cp);
$name = getUnicodeName($cp);
$output .= "{$hex} # {$name}\n";
}
return $output;
}

View file

@ -22,9 +22,9 @@
<div class="btn-list"> <div class="btn-list">
<a href="{{route('registrarcreate')}}" class="btn btn-primary d-none d-sm-inline-block"> <a href="{{route('registrarcreate')}}" class="btn btn-primary d-none d-sm-inline-block">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg> <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>
Create Registrar {{ __('Create Registrar') }}
</a> </a>
<a href="{{route('registrarcreate')}}" class="btn btn-primary d-sm-none btn-icon" aria-label="Create Registrar"> <a href="{{route('registrarcreate')}}" class="btn btn-primary d-sm-none btn-icon" aria-label="{{ __('Create Registrar') }}">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg> <svg xmlns="http://www.w3.org/2000/svg" class="icon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><line x1="12" y1="5" x2="12" y2="19" /><line x1="5" y1="12" x2="19" y2="12" /></svg>
</a> </a>
</div> </div>

View file

@ -17,6 +17,18 @@
{{ __('Manage TLD') }} {{ tld_u }} {{ __('Manage TLD') }} {{ tld_u }}
</h2> </h2>
</div> </div>
<!-- Page title actions -->
<div class="col-auto ms-auto d-print-none">
<div class="btn-list">
<a href="/registry/idnexport/{{ tld.tld }}" class="btn btn-info d-none d-sm-inline-block">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M11.5 21h-4.5a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v5m-5 6h7m-3 -3l3 3l-3 3" /></svg>
{{ __('Export IDN Table') }}
</a>
<a href="/registry/idnexport/{{ tld.tld }}" class="btn btn-info d-sm-none btn-icon" aria-label="{{ __('Export IDN Table') }}" title="{{ __('Export IDN Table') }}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 3v4a1 1 0 0 0 1 1h4" /><path d="M11.5 21h-4.5a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v5m-5 6h7m-3 -3l3 3l-3 3" /></svg>
</a>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -141,6 +141,7 @@ $app->group('', function ($route) {
$route->map(['GET', 'POST'], '/registry/tokens', SystemController::class .':manageTokens')->setName('manageTokens'); $route->map(['GET', 'POST'], '/registry/tokens', SystemController::class .':manageTokens')->setName('manageTokens');
$route->post('/registry/promotions', SystemController::class . ':managePromo')->setName('managePromo'); $route->post('/registry/promotions', SystemController::class . ':managePromo')->setName('managePromo');
$route->post('/registry/phases', SystemController::class . ':managePhases')->setName('managePhases'); $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->get('/support', SupportController::class .':view')->setName('ticketview');
$route->map(['GET', 'POST'], '/support/new', SupportController::class .':newticket')->setName('newticket'); $route->map(['GET', 'POST'], '/support/new', SupportController::class .':newticket')->setName('newticket');