Improvements in contacts UI

Also added better phone validation and made some fixes
This commit is contained in:
Pinga 2024-02-21 12:41:10 +02:00
parent d437635fef
commit 9c3ad18845
5 changed files with 106 additions and 18 deletions

View file

@ -59,7 +59,7 @@ class ContactsController extends Controller
$voice = $data['voice'] ?? null; $voice = $data['voice'] ?? null;
$fax = $data['fax'] ?? null; $fax = $data['fax'] ?? null;
$email = $data['email'] ?? null; $email = strtolower($data['email']) ?? null;
$authInfo_pw = $data['authInfo'] ?? null; $authInfo_pw = $data['authInfo'] ?? null;
if (!$contactID) { if (!$contactID) {
@ -207,15 +207,22 @@ class ContactsController extends Controller
} }
} }
if ($voice && (!preg_match('/^\+\d{1,3}\.\d{1,14}$/', $voice) || strlen($voice) > 17)) { $normalizedVoice = normalizePhoneNumber($voice, strtoupper($postalInfoIntCc));
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Voice must be (\+[0-9]{1,3}\.[0-9]{1,14})'); if (isset($normalizedVoice['error'])) {
$this->container->get('flash')->addMessage('error', 'Unable to create contact: ' . $normalizedVoice['error']);
return $response->withHeader('Location', '/contact/create')->withStatus(302); return $response->withHeader('Location', '/contact/create')->withStatus(302);
} }
$voice = $normalizedVoice['success'];
if ($fax && (!preg_match('/^\+\d{1,3}\.\d{1,14}$/', $fax) || strlen($fax) > 17)) { if (!empty($fax)) {
$this->container->get('flash')->addMessage('error', 'Unable to create contact: Fax must be (\+[0-9]{1,3}\.[0-9]{1,14})'); $normalizedFax = normalizePhoneNumber($fax, strtoupper($postalInfoIntCc));
return $response->withHeader('Location', '/contact/create')->withStatus(302); if (isset($normalizedFax['error'])) {
$this->container->get('flash')->addMessage('error', 'Unable to create contact: ' . $normalizedFax['error']);
return $response->withHeader('Location', '/contact/create')->withStatus(302);
}
// Update the fax number only if normalization was successful.
$fax = $normalizedFax['success'];
} }
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
@ -395,6 +402,8 @@ class ContactsController extends Controller
if ($contact) { if ($contact) {
$registrars = $db->selectRow('SELECT id, clid, name FROM registrar WHERE id = ?', [$contact['clid']]); $registrars = $db->selectRow('SELECT id, clid, name FROM registrar WHERE id = ?', [$contact['clid']]);
$iso3166 = new ISO3166();
$countries = $iso3166->all();
// Check if the user is not an admin (assuming role 0 is admin) // Check if the user is not an admin (assuming role 0 is admin)
if ($_SESSION["auth_roles"] != 0) { if ($_SESSION["auth_roles"] != 0) {
@ -426,7 +435,8 @@ class ContactsController extends Controller
'contactAuth' => $contactAuth, 'contactAuth' => $contactAuth,
'contactPostal' => $contactPostal, 'contactPostal' => $contactPostal,
'registrars' => $registrars, 'registrars' => $registrars,
'currentUri' => $uri 'currentUri' => $uri,
'countries' => $countries
]; ];
$verifyPhone = $db->selectValue("SELECT value FROM settings WHERE name = 'verifyPhone'"); $verifyPhone = $db->selectValue("SELECT value FROM settings WHERE name = 'verifyPhone'");

View file

@ -14,6 +14,9 @@ use MatthiasMullie\Scrapbook\Psr6\Pool;
use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\Guid\Guid; use Ramsey\Uuid\Guid\Guid;
use Ramsey\Uuid\Exception\UnsatisfiedDependencyException; use Ramsey\Uuid\Exception\UnsatisfiedDependencyException;
use libphonenumber\PhoneNumberUtil;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\NumberParseException;
/** /**
* @return mixed|string|string[] * @return mixed|string|string[]
@ -435,4 +438,43 @@ function get_client_location() {
$country = $json['country']; $country = $json['country'];
return $country; return $country;
}
function normalizePhoneNumber($number, $defaultRegion = 'US') {
$phoneUtil = PhoneNumberUtil::getInstance();
// Strip only empty spaces and dashes from the number.
$number = str_replace([' ', '-'], '', $number);
// Prepend '00' if the number does not start with '+' or '0'.
if (strpos($number, '+') !== 0 && strpos($number, '0') !== 0) {
$number = '00' . $number;
}
// Convert a leading '+' to '00' for international format compatibility.
if (strpos($number, '+') === 0) {
$number = '00' . substr($number, 1);
}
// Now, clean the number to ensure it consists only of digits.
$cleanNumber = preg_replace('/\D/', '', $number);
try {
// Parse the clean, digit-only string, which may start with '00' for international format.
$numberProto = $phoneUtil->parse($cleanNumber, $defaultRegion);
// Format the number to E.164 to ensure it includes the correct country code.
$formattedNumberE164 = $phoneUtil->format($numberProto, PhoneNumberFormat::E164);
// Extract the country code and national number.
$countryCode = $numberProto->getCountryCode();
$nationalNumber = $numberProto->getNationalNumber();
// Reconstruct the number in the desired EPP format: +CountryCode.NationalNumber
$formattedNumber = '+' . $countryCode . '.' . $nationalNumber;
return ['success' => $formattedNumber];
} catch (NumberParseException $e) {
return ['error' => 'Failed to parse and normalize phone number: ' . $e->getMessage()];
}
} }

View file

@ -57,8 +57,8 @@
<!-- Internationalized Organization --> <!-- Internationalized Organization -->
<div class="mb-3"> <div class="mb-3">
<label for="intOrg" class="form-label required">{{ __('Organization') }}</label> <label for="intOrg" class="form-label">{{ __('Organization') }}</label>
<input type="text" class="form-control" id="intOrg" name="org" required="required"> <input type="text" class="form-control" id="intOrg" name="org">
<label class="form-check form-switch mt-1"> <label class="form-check form-switch mt-1">
<input class="form-check-input" type="checkbox" id="discloseOrgInt" name="disclose_org_int"> <input class="form-check-input" type="checkbox" id="discloseOrgInt" name="disclose_org_int">
<span class="form-check-label" for="discloseOrgInt">{{ __('Disclose in WHOIS') }}</span> <span class="form-check-label" for="discloseOrgInt">{{ __('Disclose in WHOIS') }}</span>
@ -114,6 +114,7 @@
<div class="mb-3"> <div class="mb-3">
<label for="contactid" class="form-label required">{{ __('Contact ID') }}</label> <label for="contactid" class="form-label required">{{ __('Contact ID') }}</label>
<input type="text" class="form-control" id="contactid" name="contactid" required="required"> <input type="text" class="form-control" id="contactid" name="contactid" required="required">
<small class="form-text text-muted">{{ __('Auto-generated ID for the contact') }}.</small>
</div> </div>
<!-- Voice --> <!-- Voice -->
@ -280,6 +281,10 @@ document.addEventListener("DOMContentLoaded", function() {
} }
}); });
// Generate ID for Contact
const contactidInput = document.getElementById('contactid');
contactidInput.value = generateAuthInfo();
// Generate authInfo for Contact // Generate authInfo for Contact
const authInfoInput = document.getElementById('authInfo'); const authInfoInput = document.getElementById('authInfo');
authInfoInput.value = generateAuthInfo(); authInfoInput.value = generateAuthInfo();
@ -288,10 +293,29 @@ document.addEventListener("DOMContentLoaded", function() {
const length = 16; const length = 16;
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let retVal = ""; let retVal = "";
let digitCount = 0;
// Generate initial random string
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
const randomIndex = Math.floor(Math.random() * charset.length); const randomIndex = Math.floor(Math.random() * charset.length);
retVal += charset.charAt(randomIndex); const char = charset.charAt(randomIndex);
retVal += char;
if (char >= '0' && char <= '9') {
digitCount++;
}
} }
// Ensure there are at least two digits in the string
while (digitCount < 2) {
// Replace a non-digit character at a random position with a digit
const replacePosition = Math.floor(Math.random() * length);
if (!(retVal[replacePosition] >= '0' && retVal[replacePosition] <= '9')) {
const randomDigit = Math.floor(Math.random() * 10); // Generate a digit from 0 to 9
retVal = retVal.substring(0, replacePosition) + randomDigit + retVal.substring(replacePosition + 1);
digitCount++;
}
}
return retVal; return retVal;
} }
}); });

View file

@ -56,8 +56,8 @@
<!-- Internationalized Organization --> <!-- Internationalized Organization -->
<div class="mb-3"> <div class="mb-3">
<label for="intOrg" class="form-label required">{{ __('Organization') }}</label> <label for="intOrg" class="form-label">{{ __('Organization') }}</label>
<input type="text" class="form-control" id="intOrg" name="org" required="required" value="{{ postal_int.org }}"> <input type="text" class="form-control" id="intOrg" name="org" value="{{ postal_int.org }}">
<label class="form-check form-switch mt-1"> <label class="form-check form-switch mt-1">
<input class="form-check-input" type="checkbox" id="discloseOrgInt" name="disclose_org_int" {% if postal_int.disclose_org_int == '1' %}checked{% endif %}> <input class="form-check-input" type="checkbox" id="discloseOrgInt" name="disclose_org_int" {% if postal_int.disclose_org_int == '1' %}checked{% endif %}>
<span class="form-check-label" for="discloseOrgInt">{{ __('Disclose in WHOIS') }}</span> <span class="form-check-label" for="discloseOrgInt">{{ __('Disclose in WHOIS') }}</span>
@ -95,7 +95,7 @@
<label for="cc" class="form-label required">{{ __('Country') }}</label> <label for="cc" class="form-label required">{{ __('Country') }}</label>
<select class="form-select" id="cc" name="cc" required="required"> <select class="form-select" id="cc" name="cc" required="required">
{% for country in countries %} {% for country in countries %}
<option value="{{ country.alpha2 }}" {% if postal_int.cc == country.alpha2 %}selected{% endif %}>{{ country.name }}</option> <option value="{{ country.alpha2|lower }}" {% if postal_int.cc|lower == country.alpha2|lower %}selected{% endif %}>{{ country.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -252,7 +252,7 @@
<label for="locCC" class="form-label">{{ __('Country') }}</label> <label for="locCC" class="form-label">{{ __('Country') }}</label>
<select class="form-select" id="locCC" name="locCC"> <select class="form-select" id="locCC" name="locCC">
{% for country in countries %} {% for country in countries %}
<option value="{{ country.alpha2 }}" {% if postal_loc.cc == country.alpha2 %}selected{% endif %}>{{ country.name }}</option> <option value="{{ country.alpha2|lower }}" {% if postal_loc.cc|lower == country.alpha2|lower %}selected{% endif %}>{{ country.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>

View file

@ -53,7 +53,7 @@
</div> </div>
<div class="datagrid-item"> <div class="datagrid-item">
<div class="datagrid-title">{{ __('Fax') }}</div> <div class="datagrid-title">{{ __('Fax') }}</div>
<div class="datagrid-content">{{ contact.fax|default('N/A') }}</div> <div class="datagrid-content">{{ contact.fax|default('N/A') }} {% if contact.disclose_fax == '1' %}<svg xmlns="http://www.w3.org/2000/svg" class="icon text-green" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><title>{{ __('Visible in Public') }}</title><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /></svg>{% else %}<svg xmlns="http://www.w3.org/2000/svg" class="icon text-orange" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><title>{{ __('Hidden from Public') }}</title><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" /><path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" /><path d="M3 3l18 18" /></svg>{% endif %}</div>
</div> </div>
<div class="datagrid-item"> <div class="datagrid-item">
<div class="datagrid-title">{{ __('NIN') }}</div> <div class="datagrid-title">{{ __('NIN') }}</div>
@ -160,7 +160,13 @@
</div> </div>
<div class="datagrid-item"> <div class="datagrid-item">
<div class="datagrid-title">{{ __('Country') }}</div> <div class="datagrid-title">{{ __('Country') }}</div>
<div class="datagrid-content">{{ postal.cc }} {% if postal.disclose_addr_int == '1' %} <div class="datagrid-content">
{% for country in countries %}
{% if postal.cc|lower == country.alpha2|lower %}
{{ country.name }}
{% endif %}
{% endfor %}
{% if postal.disclose_addr_int == '1' %}
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-green" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><title>{{ __('Visible in Public') }}</title><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /></svg> <svg xmlns="http://www.w3.org/2000/svg" class="icon text-green" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><title>{{ __('Visible in Public') }}</title><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /></svg>
{% else %} {% else %}
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-orange" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><title>{{ __('Hidden from Public') }}</title><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" /><path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" /><path d="M3 3l18 18" /></svg> <svg xmlns="http://www.w3.org/2000/svg" class="icon text-orange" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><title>{{ __('Hidden from Public') }}</title><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" /><path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" /><path d="M3 3l18 18" /></svg>
@ -237,7 +243,13 @@
</div> </div>
<div class="datagrid-item"> <div class="datagrid-item">
<div class="datagrid-title">{{ __('Country') }}</div> <div class="datagrid-title">{{ __('Country') }}</div>
<div class="datagrid-content">{{ postal.cc }} {% if postal.disclose_addr_loc == '1' %} <div class="datagrid-content">
{% for country in countries %}
{% if postal.cc|lower == country.alpha2|lower %}
{{ country.name }}
{% endif %}
{% endfor %}
{% if postal.disclose_addr_loc == '1' %}
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-green" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><title>{{ __('Visible in Public') }}</title><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /></svg> <svg xmlns="http://www.w3.org/2000/svg" class="icon text-green" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><title>{{ __('Visible in Public') }}</title><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" /><path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" /></svg>
{% else %} {% else %}
<svg xmlns="http://www.w3.org/2000/svg" class="icon text-orange" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><title>{{ __('Hidden from Public') }}</title><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" /><path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" /><path d="M3 3l18 18" /></svg> <svg xmlns="http://www.w3.org/2000/svg" class="icon text-orange" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><title>{{ __('Hidden from Public') }}</title><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" /><path d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87" /><path d="M3 3l18 18" /></svg>