Enhance DNS discovery, validation & transfers

Add comprehensive DNS management and input validation, plus safer transfer and logging behavior.

- Add CronHelper utilities for cron scripts and unify logging/formatting.
- Improve InputValidator: sanitizeDomainInput and validateRootDomain (handles multi-level TLDs) and use throughout domain import/create flows to reject subdomains.
- DomainController: refactor DNS refresh to support quick/deep discovery (background deep scans), add endpoints to discover, add/delete/bulk-delete DNS records, import BIND zone files, enrich IP metadata via enrichIpDetails, and strengthen bulk import/reporting messages.
- DnsRecord model: add source column handling (discovered/manual/imported), avoid auto-deleting manual/imported records, and add helpers for deleting, bulk deleting, manual adding and importing zone records.
- Tag, NotificationGroup and Domain transfer logic: unlink groups when ownership changes, remove tags that belong to other users, add audit logging via Logger and improved bulk transfer reporting. TagController/View: show transferable users for admins and skip global tags on transfer.
- Notification channels (Discord, Mattermost, etc.) and EmailHelper: allow explicit subjects and improve payload fields based on notification type.
- Add new migration 029_add_dns_record_source.sql and wire it into the installer; update migrations detection.
- Add new views/partials for confirm/import/transfer modals, update various domain/group/tag templates, and update cron scripts and routes for discovery.

These changes preserve manual/imported DNS records, improve root-domain validation, enable background deep discovery, and add better logging/audit trails for transfers and imports.
This commit is contained in:
Hosteroid
2026-03-10 22:54:28 +02:00
parent 5365af00fd
commit a265a58456
46 changed files with 3130 additions and 1494 deletions

View File

@@ -17,17 +17,90 @@ class InputValidator
*/
public static function validateDomain(string $domain): bool
{
// Check length (max 253 characters per RFC 1035)
if (strlen($domain) > 253 || strlen($domain) < 3) {
return false;
}
// Validate domain format
// Allows: example.com, sub.example.com, example.co.uk
// Pattern: alphanumeric with hyphens, dots between labels, valid TLD
return (bool)preg_match('/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}$/i', $domain);
}
/**
* Sanitize raw domain input — strips protocol, www prefix, trailing dots/slashes, paths.
*
* @return string Cleaned, lowercased domain (may still be invalid)
*/
public static function sanitizeDomainInput(string $input): string
{
$input = strtolower(trim($input));
$input = preg_replace('#^https?://#', '', $input);
$input = preg_replace('#/.*$#', '', $input);
$input = rtrim($input, '.');
$input = trim($input);
if (str_starts_with($input, 'www.')) {
$stripped = substr($input, 4);
if (substr_count($stripped, '.') >= 1) {
$input = $stripped;
}
}
return $input;
}
/**
* Validate that a domain is a registrable root domain (not a subdomain).
* Uses the tld_registry table to identify multi-level TLDs like .co.uk.
*
* @return array{valid: bool, domain: string, error: string|null}
*/
public static function validateRootDomain(string $domain): array
{
$domain = self::sanitizeDomainInput($domain);
if (empty($domain)) {
return ['valid' => false, 'domain' => '', 'error' => 'Domain name is required'];
}
if (!self::validateDomain($domain)) {
return ['valid' => false, 'domain' => $domain, 'error' => "Invalid domain format: $domain"];
}
$parts = explode('.', $domain);
if (count($parts) < 2) {
return ['valid' => false, 'domain' => $domain, 'error' => "Invalid domain: $domain"];
}
$tldModel = new \App\Models\TldRegistry();
$matchedTld = null;
for ($i = 1; $i < count($parts); $i++) {
$candidate = '.' . implode('.', array_slice($parts, $i));
$tld = $tldModel->findByTld($candidate);
if ($tld) {
$matchedTld = $candidate;
$labelCount = $i;
break;
}
}
if (!$matchedTld) {
$matchedTld = '.' . $parts[count($parts) - 1];
$labelCount = count($parts) - 1;
}
if ($labelCount !== 1) {
$rootDomain = $parts[$labelCount - 1] . $matchedTld;
return [
'valid' => false,
'domain' => $domain,
'error' => "\"$domain\" looks like a subdomain. Did you mean the root domain \"$rootDomain\"?"
];
}
return ['valid' => true, 'domain' => $domain, 'error' => null];
}
/**
* Validate text field length
*