* @copyright 2026 Bartek Janczak * @license Custom Source-Available License (see LICENSE file) */ namespace Bjanczak\FilamentShortUrl\Models; use App\Models\User; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Support\Carbon; /** * @property int $id * @property int|null $user_id * @property string $domain * @property bool $is_verified * @property bool $is_active * @property Carbon $created_at * @property Carbon $updated_at */ class ShortUrlCustomDomain extends Model { protected $table = 'short_url_custom_domains'; protected $fillable = [ 'domain', 'is_verified', 'is_active', 'user_id', ]; protected $casts = [ 'is_verified' => 'boolean', 'is_active' => 'boolean', ]; /** * Get the owner of this custom domain. */ public function user(): BelongsTo { $userModel = config('filament-short-url.user.model', User::class); return $this->belongsTo($userModel, 'user_id'); } /** * Get short URLs mapped to this custom domain. */ public function shortUrls(): HasMany { return $this->hasMany(ShortUrl::class, 'custom_domain_id'); } /** * Invalidate the redirect-layer custom-domain cache on any state change. * This ensures that verifying, deactivating, or deleting a domain is reflected * immediately without waiting for the 5-minute cache TTL to expire. */ protected static function booted(): void { static::saved(function (self $m): void { cache()->forget("filament-short-url:custom-domain:{$m->domain}"); $domainChanged = $m->wasChanged('domain'); if ($domainChanged) { $oldDomain = $m->getOriginal('domain'); if ($oldDomain) { cache()->forget("filament-short-url:custom-domain:{$oldDomain}"); } } // Invalidate the redirect cache keys for all short URLs mapped to this domain // if the domain name changes or its active status is toggled. if ($domainChanged || $m->wasChanged('is_active')) { $hosts = [$m->domain]; if ($domainChanged && isset($oldDomain)) { $hosts[] = $oldDomain; } $shortUrls = $m->shortUrls()->get(['url_key']); foreach ($shortUrls as $url) { foreach ($hosts as $host) { cache()->forget("filament-short-url:{$url->url_key}:{$host}"); } } } }); static::deleted(function (self $m): void { cache()->forget("filament-short-url:custom-domain:{$m->domain}"); // Clear redirect caches for all URLs mapped to this deleted domain $shortUrls = $m->shortUrls()->get(['url_key']); foreach ($shortUrls as $url) { cache()->forget("filament-short-url:{$url->url_key}:{$m->domain}"); } }); } /** * Dynamically resolve DNS records for this custom domain to verify if it points * back to the host application server (via CNAME or A record). */ public function verifyDns(): bool { $domain = trim(strtolower($this->domain)); if (empty($domain)) { return false; } // Get target host domain dynamically from configuration $hostDomain = parse_url(config('app.url'), PHP_URL_HOST); if (empty($hostDomain)) { $hostDomain = request()?->getHost(); } if (empty($hostDomain)) { return false; } $hostDomain = trim(strtolower($hostDomain)); // 1. Resolve host server IP dynamically $hostIp = null; try { if (function_exists('dns_get_record')) { $records = @dns_get_record($hostDomain, DNS_A); if (is_array($records) && ! empty($records)) { $hostIp = $records[0]['ip'] ?? null; } } if (empty($hostIp)) { $output = shell_exec('dig +short +time=1 +tries=1 A '.escapeshellarg($hostDomain)); if ($output) { $ips = array_filter(array_map('trim', explode("\n", trim($output)))); $hostIp = reset($ips) ?: null; } } } catch (\Throwable $e) { // ignore } if (empty($hostIp)) { $hostIp = $_SERVER['SERVER_ADDR'] ?? null; } $isVerified = false; // 2. Resolve domain A records $aIps = []; try { if (function_exists('dns_get_record')) { $records = @dns_get_record($domain, DNS_A); if (is_array($records)) { $aIps = array_filter(array_column($records, 'ip')); } } if (empty($aIps)) { $output = shell_exec('dig +short +time=1 +tries=1 A '.escapeshellarg($domain)); if ($output) { $aIps = array_filter(array_map('trim', explode("\n", trim($output)))); } } } catch (\Throwable $e) { // ignore } if (! empty($aIps) && $hostIp && in_array($hostIp, $aIps)) { $isVerified = true; } // 3. Check CNAME record if (! $isVerified) { $cnameTarget = null; try { if (function_exists('dns_get_record')) { $records = @dns_get_record($domain, DNS_CNAME); if (is_array($records) && ! empty($records)) { $cnameTarget = trim(strtolower(rtrim($records[0]['target'] ?? '', '.'))); } } if (empty($cnameTarget)) { $output = shell_exec('dig +short +time=1 +tries=1 CNAME '.escapeshellarg($domain)); if ($output) { $cnameTarget = trim(strtolower(rtrim(trim($output), '.'))); } } } catch (\Throwable $e) { // ignore } if ($cnameTarget && strcasecmp($cnameTarget, $hostDomain) === 0) { $isVerified = true; } } if ($this->exists && $this->is_verified !== $isVerified) { $this->update(['is_verified' => $isVerified]); } return $isVerified; } }