mirror of
https://github.com/getnamingo/registry.git
synced 2025-07-23 19:10:30 +02:00
Added ability to export IDN tables, fixed #193
This commit is contained in:
parent
89ac087a3d
commit
955dd6ffe0
5 changed files with 357 additions and 3 deletions
|
@ -983,7 +983,7 @@ class SystemController extends Controller
|
|||
// Mapping of regex patterns to script names
|
||||
$regexToScriptName = [
|
||||
'/^(?!-)(?!.*--)[A-Z0-9-]{1,63}(?<!-)(.(?!-)(?!.*--)[A-Z0-9-]{1,63}(?<!-))*$/i' => '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}(?<!-)$/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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -22,9 +22,9 @@
|
|||
<div class="btn-list">
|
||||
<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>
|
||||
Create Registrar
|
||||
{{ __('Create Registrar') }}
|
||||
</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>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -17,6 +17,18 @@
|
|||
{{ __('Manage TLD') }} {{ tld_u }}
|
||||
</h2>
|
||||
</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>
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue