Initial Commit
This commit is contained in:
100
app/Services/Channels/DiscordChannel.php
Normal file
100
app/Services/Channels/DiscordChannel.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Channels;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class DiscordChannel implements NotificationChannelInterface
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client(['timeout' => 10]);
|
||||
}
|
||||
|
||||
public function send(array $config, string $message, array $data = []): bool
|
||||
{
|
||||
if (!isset($config['webhook_url'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$embed = $this->createEmbed($message, $data);
|
||||
|
||||
$response = $this->client->post($config['webhook_url'], [
|
||||
'json' => [
|
||||
'embeds' => [$embed]
|
||||
]
|
||||
]);
|
||||
|
||||
return $response->getStatusCode() === 204;
|
||||
} catch (\Exception $e) {
|
||||
error_log("Discord send failed: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function createEmbed(string $message, array $data): array
|
||||
{
|
||||
$color = $this->getColorByDaysLeft($data['days_left'] ?? null);
|
||||
|
||||
$embed = [
|
||||
'title' => '🔔 Domain Expiration Alert',
|
||||
'description' => $message,
|
||||
'color' => $color,
|
||||
'timestamp' => date('c'),
|
||||
'footer' => [
|
||||
'text' => 'Domain Monitor'
|
||||
]
|
||||
];
|
||||
|
||||
if (isset($data['domain'])) {
|
||||
$embed['fields'] = [
|
||||
[
|
||||
'name' => 'Domain',
|
||||
'value' => $data['domain'],
|
||||
'inline' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Days Left',
|
||||
'value' => $data['days_left'],
|
||||
'inline' => true
|
||||
],
|
||||
[
|
||||
'name' => 'Expiration Date',
|
||||
'value' => $data['expiration_date'],
|
||||
'inline' => true
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return $embed;
|
||||
}
|
||||
|
||||
private function getColorByDaysLeft(?int $daysLeft): int
|
||||
{
|
||||
if ($daysLeft === null) {
|
||||
return 0x808080; // Gray
|
||||
}
|
||||
|
||||
if ($daysLeft <= 0) {
|
||||
return 0xFF0000; // Red
|
||||
}
|
||||
|
||||
if ($daysLeft <= 3) {
|
||||
return 0xFF4500; // Orange Red
|
||||
}
|
||||
|
||||
if ($daysLeft <= 7) {
|
||||
return 0xFFA500; // Orange
|
||||
}
|
||||
|
||||
if ($daysLeft <= 30) {
|
||||
return 0xFFFF00; // Yellow
|
||||
}
|
||||
|
||||
return 0x00FF00; // Green
|
||||
}
|
||||
}
|
||||
|
||||
91
app/Services/Channels/EmailChannel.php
Normal file
91
app/Services/Channels/EmailChannel.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Channels;
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
class EmailChannel implements NotificationChannelInterface
|
||||
{
|
||||
public function send(array $config, string $message, array $data = []): bool
|
||||
{
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
try {
|
||||
// Server settings
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $_ENV['MAIL_HOST'];
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = $_ENV['MAIL_USERNAME'];
|
||||
$mail->Password = $_ENV['MAIL_PASSWORD'];
|
||||
$mail->SMTPSecure = $_ENV['MAIL_ENCRYPTION'];
|
||||
$mail->Port = $_ENV['MAIL_PORT'];
|
||||
|
||||
// Recipients
|
||||
$mail->setFrom($_ENV['MAIL_FROM_ADDRESS'], $_ENV['MAIL_FROM_NAME']);
|
||||
$mail->addAddress($config['email']);
|
||||
|
||||
// Content
|
||||
$mail->isHTML(true);
|
||||
$mail->Subject = $this->getSubject($data);
|
||||
$mail->Body = $this->formatHtmlBody($message, $data);
|
||||
$mail->AltBody = strip_tags($message);
|
||||
|
||||
$mail->send();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
error_log("Email send failed: {$mail->ErrorInfo}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function getSubject(array $data): string
|
||||
{
|
||||
if (isset($data['domain'])) {
|
||||
$daysLeft = $data['days_left'];
|
||||
if ($daysLeft <= 0) {
|
||||
return "🚨 URGENT: Domain {$data['domain']} has EXPIRED";
|
||||
}
|
||||
if ($daysLeft == 1) {
|
||||
return "⚠️ CRITICAL: Domain {$data['domain']} expires TOMORROW";
|
||||
}
|
||||
return "⚠️ Domain Expiration Alert: {$data['domain']} ({$daysLeft} days)";
|
||||
}
|
||||
|
||||
return "Domain Monitor Alert";
|
||||
}
|
||||
|
||||
private function formatHtmlBody(string $message, array $data): string
|
||||
{
|
||||
$messageHtml = nl2br(htmlspecialchars($message));
|
||||
|
||||
return "
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
||||
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.header { background: #4A90E2; color: white; padding: 20px; border-radius: 5px 5px 0 0; }
|
||||
.content { background: #f9f9f9; padding: 20px; border: 1px solid #ddd; }
|
||||
.footer { background: #333; color: white; padding: 10px; text-align: center; font-size: 12px; border-radius: 0 0 5px 5px; }
|
||||
.button { display: inline-block; padding: 10px 20px; background: #4A90E2; color: white; text-decoration: none; border-radius: 5px; margin-top: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class='container'>
|
||||
<div class='header'>
|
||||
<h2>🔔 Domain Monitor Alert</h2>
|
||||
</div>
|
||||
<div class='content'>
|
||||
<p>$messageHtml</p>
|
||||
</div>
|
||||
<div class='footer'>
|
||||
<p>This is an automated message from Domain Monitor</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
";
|
||||
}
|
||||
}
|
||||
|
||||
17
app/Services/Channels/NotificationChannelInterface.php
Normal file
17
app/Services/Channels/NotificationChannelInterface.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Channels;
|
||||
|
||||
interface NotificationChannelInterface
|
||||
{
|
||||
/**
|
||||
* Send notification through the channel
|
||||
*
|
||||
* @param array $config Channel-specific configuration
|
||||
* @param string $message Message to send
|
||||
* @param array $data Additional data for formatting
|
||||
* @return bool Success status
|
||||
*/
|
||||
public function send(array $config, string $message, array $data = []): bool;
|
||||
}
|
||||
|
||||
85
app/Services/Channels/SlackChannel.php
Normal file
85
app/Services/Channels/SlackChannel.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Channels;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class SlackChannel implements NotificationChannelInterface
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client(['timeout' => 10]);
|
||||
}
|
||||
|
||||
public function send(array $config, string $message, array $data = []): bool
|
||||
{
|
||||
if (!isset($config['webhook_url'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = [
|
||||
'text' => $message,
|
||||
'blocks' => $this->createBlocks($message, $data)
|
||||
];
|
||||
|
||||
$response = $this->client->post($config['webhook_url'], [
|
||||
'json' => $payload
|
||||
]);
|
||||
|
||||
return $response->getStatusCode() === 200;
|
||||
} catch (\Exception $e) {
|
||||
error_log("Slack send failed: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function createBlocks(string $message, array $data): array
|
||||
{
|
||||
$blocks = [
|
||||
[
|
||||
'type' => 'header',
|
||||
'text' => [
|
||||
'type' => 'plain_text',
|
||||
'text' => '🔔 Domain Expiration Alert'
|
||||
]
|
||||
],
|
||||
[
|
||||
'type' => 'section',
|
||||
'text' => [
|
||||
'type' => 'mrkdwn',
|
||||
'text' => $message
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
if (isset($data['domain'])) {
|
||||
$blocks[] = [
|
||||
'type' => 'section',
|
||||
'fields' => [
|
||||
[
|
||||
'type' => 'mrkdwn',
|
||||
'text' => "*Domain:*\n{$data['domain']}"
|
||||
],
|
||||
[
|
||||
'type' => 'mrkdwn',
|
||||
'text' => "*Days Left:*\n{$data['days_left']}"
|
||||
],
|
||||
[
|
||||
'type' => 'mrkdwn',
|
||||
'text' => "*Expiration:*\n{$data['expiration_date']}"
|
||||
],
|
||||
[
|
||||
'type' => 'mrkdwn',
|
||||
'text' => "*Registrar:*\n{$data['registrar']}"
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return $blocks;
|
||||
}
|
||||
}
|
||||
|
||||
42
app/Services/Channels/TelegramChannel.php
Normal file
42
app/Services/Channels/TelegramChannel.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Channels;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class TelegramChannel implements NotificationChannelInterface
|
||||
{
|
||||
private Client $client;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Client([
|
||||
'base_uri' => 'https://api.telegram.org',
|
||||
'timeout' => 10,
|
||||
]);
|
||||
}
|
||||
|
||||
public function send(array $config, string $message, array $data = []): bool
|
||||
{
|
||||
if (!isset($config['bot_token']) || !isset($config['chat_id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->client->post("/bot{$config['bot_token']}/sendMessage", [
|
||||
'json' => [
|
||||
'chat_id' => $config['chat_id'],
|
||||
'text' => $message,
|
||||
'parse_mode' => 'HTML',
|
||||
'disable_web_page_preview' => true,
|
||||
]
|
||||
]);
|
||||
|
||||
return $response->getStatusCode() === 200;
|
||||
} catch (\Exception $e) {
|
||||
error_log("Telegram send failed: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
155
app/Services/Logger.php
Normal file
155
app/Services/Logger.php
Normal file
@@ -0,0 +1,155 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
class Logger
|
||||
{
|
||||
private string $logDir;
|
||||
private string $currentLogFile;
|
||||
private bool $enabled;
|
||||
|
||||
public function __construct(string $logName = 'app', bool $enabled = true)
|
||||
{
|
||||
$this->logDir = __DIR__ . '/../../logs';
|
||||
$this->enabled = $enabled;
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
if (!is_dir($this->logDir)) {
|
||||
mkdir($this->logDir, 0755, true);
|
||||
}
|
||||
|
||||
// Set log file name with date
|
||||
$date = date('Y-m-d');
|
||||
$this->currentLogFile = $this->logDir . '/' . $logName . '_' . $date . '.log';
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message with level
|
||||
*/
|
||||
public function log(string $level, string $message, array $context = []): void
|
||||
{
|
||||
if (!$this->enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$contextStr = !empty($context) ? ' | Context: ' . json_encode($context) : '';
|
||||
$logLine = "[{$timestamp}] [{$level}] {$message}{$contextStr}\n";
|
||||
|
||||
file_put_contents($this->currentLogFile, $logLine, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log debug message
|
||||
*/
|
||||
public function debug(string $message, array $context = []): void
|
||||
{
|
||||
$this->log('DEBUG', $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log info message
|
||||
*/
|
||||
public function info(string $message, array $context = []): void
|
||||
{
|
||||
$this->log('INFO', $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log warning message
|
||||
*/
|
||||
public function warning(string $message, array $context = []): void
|
||||
{
|
||||
$this->log('WARNING', $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error message
|
||||
*/
|
||||
public function error(string $message, array $context = []): void
|
||||
{
|
||||
$this->log('ERROR', $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log critical message
|
||||
*/
|
||||
public function critical(string $message, array $context = []): void
|
||||
{
|
||||
$this->log('CRITICAL', $message, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log progress with percentage
|
||||
*/
|
||||
public function progress(string $message, int $current, int $total, array $context = []): void
|
||||
{
|
||||
$percentage = $total > 0 ? round(($current / $total) * 100, 2) : 0;
|
||||
$progressMessage = "{$message} [{$current}/{$total}] ({$percentage}%)";
|
||||
$this->info($progressMessage, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log separator for better readability
|
||||
*/
|
||||
public function separator(string $title = ''): void
|
||||
{
|
||||
$line = str_repeat('=', 80);
|
||||
if (!empty($title)) {
|
||||
$titleLine = "=== {$title} " . str_repeat('=', 80 - strlen($title) - 5);
|
||||
$this->log('INFO', $titleLine);
|
||||
} else {
|
||||
$this->log('INFO', $line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log start of operation
|
||||
*/
|
||||
public function startOperation(string $operation, array $context = []): void
|
||||
{
|
||||
$this->separator("START: {$operation}");
|
||||
$this->info("Starting operation: {$operation}", $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log end of operation
|
||||
*/
|
||||
public function endOperation(string $operation, array $stats = []): void
|
||||
{
|
||||
$this->info("Completed operation: {$operation}", $stats);
|
||||
$this->separator("END: {$operation}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log file path
|
||||
*/
|
||||
public function getLogFile(): string
|
||||
{
|
||||
return $this->currentLogFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear current log file
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
if (file_exists($this->currentLogFile)) {
|
||||
unlink($this->currentLogFile);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read last N lines from log file
|
||||
*/
|
||||
public function tail(int $lines = 100): array
|
||||
{
|
||||
if (!file_exists($this->currentLogFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$file = file($this->currentLogFile);
|
||||
return array_slice($file, -$lines);
|
||||
}
|
||||
}
|
||||
|
||||
153
app/Services/NotificationService.php
Normal file
153
app/Services/NotificationService.php
Normal file
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Services\Channels\EmailChannel;
|
||||
use App\Services\Channels\TelegramChannel;
|
||||
use App\Services\Channels\DiscordChannel;
|
||||
use App\Services\Channels\SlackChannel;
|
||||
|
||||
class NotificationService
|
||||
{
|
||||
private array $channels = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->channels = [
|
||||
'email' => new EmailChannel(),
|
||||
'telegram' => new TelegramChannel(),
|
||||
'discord' => new DiscordChannel(),
|
||||
'slack' => new SlackChannel(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to specified channel
|
||||
*/
|
||||
public function send(string $channelType, array $config, string $message, array $data = []): bool
|
||||
{
|
||||
if (!isset($this->channels[$channelType])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->channels[$channelType]->send($config, $message, $data);
|
||||
} catch (\Exception $e) {
|
||||
error_log("Notification send failed [$channelType]: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification to all active channels in a group
|
||||
*/
|
||||
public function sendToGroup(int $groupId, string $subject, string $message, array $data = []): array
|
||||
{
|
||||
// Get active channels for the group
|
||||
$channelModel = new \App\Models\NotificationChannel();
|
||||
$channels = $channelModel->getByGroupId($groupId);
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($channels as $channel) {
|
||||
if (!$channel['is_active']) {
|
||||
continue; // Skip inactive channels
|
||||
}
|
||||
|
||||
$config = json_decode($channel['channel_config'], true);
|
||||
|
||||
// Add subject to data for channels that support it (like email)
|
||||
$channelData = array_merge(['subject' => $subject], $data);
|
||||
|
||||
$success = $this->send(
|
||||
$channel['channel_type'],
|
||||
$config,
|
||||
$message,
|
||||
$channelData
|
||||
);
|
||||
|
||||
$results[] = [
|
||||
'channel' => $channel['channel_type'],
|
||||
'success' => $success
|
||||
];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send domain expiration notification
|
||||
*/
|
||||
public function sendDomainExpirationAlert(array $domain, array $notificationChannels): array
|
||||
{
|
||||
$daysLeft = $this->calculateDaysLeft($domain['expiration_date']);
|
||||
$message = $this->formatExpirationMessage($domain, $daysLeft);
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($notificationChannels as $channel) {
|
||||
$config = json_decode($channel['channel_config'], true);
|
||||
$success = $this->send(
|
||||
$channel['channel_type'],
|
||||
$config,
|
||||
$message,
|
||||
[
|
||||
'domain' => $domain['domain_name'],
|
||||
'days_left' => $daysLeft,
|
||||
'expiration_date' => $domain['expiration_date'],
|
||||
'registrar' => $domain['registrar']
|
||||
]
|
||||
);
|
||||
|
||||
$results[] = [
|
||||
'channel' => $channel['channel_type'],
|
||||
'success' => $success
|
||||
];
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format expiration message
|
||||
*/
|
||||
private function formatExpirationMessage(array $domain, int $daysLeft): string
|
||||
{
|
||||
$domainName = $domain['domain_name'];
|
||||
$expirationDate = date('F j, Y', strtotime($domain['expiration_date']));
|
||||
$registrar = $domain['registrar'] ?? 'Unknown';
|
||||
|
||||
if ($daysLeft <= 0) {
|
||||
return "🚨 URGENT: Domain '$domainName' has EXPIRED on $expirationDate!\n\n" .
|
||||
"Registrar: $registrar\n" .
|
||||
"Please renew immediately to avoid losing your domain.";
|
||||
}
|
||||
|
||||
if ($daysLeft == 1) {
|
||||
return "⚠️ CRITICAL: Domain '$domainName' expires TOMORROW ($expirationDate)!\n\n" .
|
||||
"Registrar: $registrar\n" .
|
||||
"Please renew as soon as possible.";
|
||||
}
|
||||
|
||||
if ($daysLeft <= 7) {
|
||||
return "⚠️ WARNING: Domain '$domainName' expires in $daysLeft days ($expirationDate)!\n\n" .
|
||||
"Registrar: $registrar\n" .
|
||||
"Please renew soon.";
|
||||
}
|
||||
|
||||
return "ℹ️ REMINDER: Domain '$domainName' expires in $daysLeft days ($expirationDate).\n\n" .
|
||||
"Registrar: $registrar\n" .
|
||||
"Please plan for renewal.";
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate days left until expiration
|
||||
*/
|
||||
private function calculateDaysLeft(string $expirationDate): int
|
||||
{
|
||||
$expiration = strtotime($expirationDate);
|
||||
$now = time();
|
||||
return (int)floor(($expiration - $now) / 86400);
|
||||
}
|
||||
}
|
||||
|
||||
1866
app/Services/TldRegistryService.php
Normal file
1866
app/Services/TldRegistryService.php
Normal file
File diff suppressed because it is too large
Load Diff
729
app/Services/WhoisService.php
Normal file
729
app/Services/WhoisService.php
Normal file
@@ -0,0 +1,729 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Exception;
|
||||
use App\Models\TldRegistry;
|
||||
|
||||
class WhoisService
|
||||
{
|
||||
// Cache for discovered TLD servers to avoid repeated IANA queries
|
||||
private static array $tldCache = [];
|
||||
private TldRegistry $tldModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->tldModel = new TldRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain information via WHOIS or RDAP
|
||||
*/
|
||||
public function getDomainInfo(string $domain): ?array
|
||||
{
|
||||
try {
|
||||
// Get TLD
|
||||
$parts = explode('.', $domain);
|
||||
if (count($parts) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle double TLDs like co.uk
|
||||
$tld = $parts[count($parts) - 1];
|
||||
$doubleTld = null;
|
||||
if (count($parts) >= 3) {
|
||||
$doubleTld = $parts[count($parts) - 2] . '.' . $tld;
|
||||
}
|
||||
|
||||
// Try double TLD first (e.g., co.uk), then single TLD
|
||||
$servers = null;
|
||||
if ($doubleTld) {
|
||||
$servers = $this->discoverTldServers($doubleTld);
|
||||
// If double TLD lookup failed, try single TLD
|
||||
if (!$servers['rdap_url'] && !$servers['whois_server']) {
|
||||
$servers = $this->discoverTldServers($tld);
|
||||
}
|
||||
} else {
|
||||
$servers = $this->discoverTldServers($tld);
|
||||
}
|
||||
|
||||
$rdapUrl = $servers['rdap_url'];
|
||||
$whoisServer = $servers['whois_server'];
|
||||
|
||||
// Try RDAP first (modern, structured JSON protocol)
|
||||
if ($rdapUrl) {
|
||||
$rdapData = $this->queryRDAPGeneric($domain, $rdapUrl);
|
||||
if ($rdapData) {
|
||||
return $rdapData;
|
||||
}
|
||||
// If RDAP failed, fall through to WHOIS
|
||||
}
|
||||
|
||||
// Fallback to WHOIS if RDAP not available or failed
|
||||
if (!$whoisServer) {
|
||||
$whoisServer = 'whois.iana.org';
|
||||
}
|
||||
|
||||
// Get WHOIS data
|
||||
$whoisData = $this->queryWhois($domain, $whoisServer);
|
||||
|
||||
if (!$whoisData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we got a referral to another WHOIS server
|
||||
$referralServer = $this->extractReferralServer($whoisData);
|
||||
if ($referralServer && $referralServer !== $whoisServer) {
|
||||
// Query the referred server
|
||||
$whoisData = $this->queryWhois($domain, $referralServer);
|
||||
if (!$whoisData) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the response
|
||||
$info = $this->parseWhoisData($domain, $whoisData, $referralServer ?? $whoisServer);
|
||||
|
||||
return $info;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("WHOIS lookup failed for $domain: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover RDAP and WHOIS servers for a TLD using TLD registry data
|
||||
* Returns array with 'rdap_url' and 'whois_server' keys
|
||||
*/
|
||||
private function discoverTldServers(string $tld): array
|
||||
{
|
||||
// Check cache first
|
||||
if (isset(self::$tldCache[$tld])) {
|
||||
return self::$tldCache[$tld];
|
||||
}
|
||||
|
||||
$result = [
|
||||
'rdap_url' => null,
|
||||
'whois_server' => null
|
||||
];
|
||||
|
||||
try {
|
||||
// First, try to get TLD info from our registry database
|
||||
$tldInfo = $this->tldModel->getByTld($tld);
|
||||
|
||||
if ($tldInfo) {
|
||||
// Use WHOIS server from registry
|
||||
if (!empty($tldInfo['whois_server'])) {
|
||||
$result['whois_server'] = $tldInfo['whois_server'];
|
||||
}
|
||||
|
||||
// Use RDAP servers from registry
|
||||
if (!empty($tldInfo['rdap_servers'])) {
|
||||
$rdapServers = json_decode($tldInfo['rdap_servers'], true);
|
||||
if (is_array($rdapServers) && !empty($rdapServers)) {
|
||||
$result['rdap_url'] = rtrim($rdapServers[0], '/') . '/';
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
self::$tldCache[$tld] = $result;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Fallback: Query IANA directly if not in our registry
|
||||
// This maintains backward compatibility and handles new TLDs
|
||||
$response = $this->queryWhois($tld, 'whois.iana.org');
|
||||
|
||||
if (!$response) {
|
||||
self::$tldCache[$tld] = $result;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Parse IANA response for WHOIS server
|
||||
$lines = explode("\n", $response);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Look for WHOIS server
|
||||
if (preg_match('/^whois:\s+(.+)$/i', $line, $matches)) {
|
||||
$result['whois_server'] = trim($matches[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for .pro TLD - it doesn't have a WHOIS server in IANA
|
||||
if ($tld === 'pro' && !$result['whois_server']) {
|
||||
$result['whois_server'] = 'whois.afilias.net';
|
||||
}
|
||||
|
||||
// Try to get RDAP URL from IANA's RDAP bootstrap service
|
||||
$rdapBootstrapUrl = "https://data.iana.org/rdap/dns.json";
|
||||
$bootstrapData = @file_get_contents($rdapBootstrapUrl, false, stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 5,
|
||||
'user_agent' => 'Domain Monitor/1.0'
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true
|
||||
]
|
||||
]));
|
||||
|
||||
if ($bootstrapData) {
|
||||
$bootstrap = json_decode($bootstrapData, true);
|
||||
if ($bootstrap && isset($bootstrap['services'])) {
|
||||
// The services array contains [["tld1", "tld2"], ["url1", "url2"]]
|
||||
foreach ($bootstrap['services'] as $service) {
|
||||
if (isset($service[0]) && isset($service[1])) {
|
||||
$tlds = $service[0];
|
||||
$urls = $service[1];
|
||||
|
||||
// Check if our TLD is in this service's TLD list
|
||||
if (in_array($tld, $tlds) || in_array('.' . $tld, $tlds)) {
|
||||
if (!empty($urls[0])) {
|
||||
$result['rdap_url'] = rtrim($urls[0], '/') . '/';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try fetching the HTML page from IANA
|
||||
if (!$result['rdap_url']) {
|
||||
$htmlUrl = "https://www.iana.org/domains/root/db/{$tld}.html";
|
||||
$html = @file_get_contents($htmlUrl, false, stream_context_create([
|
||||
'http' => [
|
||||
'timeout' => 5,
|
||||
'user_agent' => 'Domain Monitor/1.0'
|
||||
],
|
||||
'ssl' => [
|
||||
'verify_peer' => true,
|
||||
'verify_peer_name' => true
|
||||
]
|
||||
]));
|
||||
|
||||
if ($html) {
|
||||
// Extract RDAP Server from HTML
|
||||
// Pattern: <b>RDAP Server:</b> https://rdap.example.com/
|
||||
if (preg_match('/<b>RDAP Server:<\/b>\s*<a[^>]*>(https?:\/\/[^<]+)<\/a>/i', $html, $matches)) {
|
||||
$result['rdap_url'] = rtrim(trim($matches[1]), '/') . '/';
|
||||
} elseif (preg_match('/<b>RDAP Server:<\/b>\s+(https?:\/\/\S+)/i', $html, $matches)) {
|
||||
$result['rdap_url'] = rtrim(trim($matches[1]), '/') . '/';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DO NOT guess RDAP URLs - they must be from official sources
|
||||
// Guessing often creates invalid URLs that don't resolve in DNS
|
||||
|
||||
// Cache the result
|
||||
self::$tldCache[$tld] = $result;
|
||||
|
||||
return $result;
|
||||
} catch (Exception $e) {
|
||||
self::$tldCache[$tld] = $result;
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extract referral WHOIS server from response
|
||||
*/
|
||||
private function extractReferralServer(string $whoisData): ?string
|
||||
{
|
||||
$lines = explode("\n", $whoisData);
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Check for various referral patterns
|
||||
if (preg_match('/^Registrar WHOIS Server:\s*(.+)$/i', $line, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
if (preg_match('/^ReferralServer:\s*whois:\/\/(.+)$/i', $line, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
if (preg_match('/^refer:\s*(.+)$/i', $line, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
if (preg_match('/^whois server:\s*(.+)$/i', $line, $matches)) {
|
||||
$server = trim($matches[1]);
|
||||
// Skip if it's just 'whois.iana.org' (we already queried that)
|
||||
if ($server !== 'whois.iana.org') {
|
||||
return $server;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query generic RDAP server for any domain
|
||||
*/
|
||||
private function queryRDAPGeneric(string $domain, string $rdapBaseUrl): ?array
|
||||
{
|
||||
// Ensure URL ends with /
|
||||
if (substr($rdapBaseUrl, -1) !== '/') {
|
||||
$rdapBaseUrl .= '/';
|
||||
}
|
||||
|
||||
// Construct full RDAP URL
|
||||
// RDAP standard format: {base_url}domain/{domain_name}
|
||||
// If the base URL doesn't already end with "domain/", add it
|
||||
if (!preg_match('/domain\/$/', $rdapBaseUrl)) {
|
||||
$rdapUrl = $rdapBaseUrl . 'domain/' . strtolower($domain);
|
||||
} else {
|
||||
$rdapUrl = $rdapBaseUrl . strtolower($domain);
|
||||
}
|
||||
|
||||
// Use cURL to get RDAP data
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $rdapUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Accept: application/rdap+json'
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
// Handle 404 responses as domain not found
|
||||
if ($httpCode === 404 && $response) {
|
||||
$data = json_decode($response, true);
|
||||
if ($data && isset($data['errorCode']) && $data['errorCode'] == 404) {
|
||||
// Return domain not found response
|
||||
$rdapHost = parse_url($rdapBaseUrl, PHP_URL_HOST);
|
||||
return [
|
||||
'domain' => $domain,
|
||||
'registrar' => 'Not Registered',
|
||||
'registrar_url' => null,
|
||||
'expiration_date' => null,
|
||||
'updated_date' => null,
|
||||
'creation_date' => null,
|
||||
'abuse_email' => null,
|
||||
'nameservers' => [],
|
||||
'status' => ['AVAILABLE'],
|
||||
'owner' => 'Unknown',
|
||||
'whois_server' => $rdapHost . ' (RDAP)',
|
||||
'raw_data' => [
|
||||
'states' => ['AVAILABLE'],
|
||||
'nameServers' => [],
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($httpCode !== 200 || !$response) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$data = json_decode($response, true);
|
||||
if (!$data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract the RDAP host for display
|
||||
$rdapHost = parse_url($rdapBaseUrl, PHP_URL_HOST);
|
||||
|
||||
return $this->parseRDAPData($domain, $data, $rdapHost);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse RDAP JSON data into our standard format
|
||||
*/
|
||||
private function parseRDAPData(string $domain, array $rdapData, string $rdapHost = 'RDAP'): array
|
||||
{
|
||||
$info = [
|
||||
'domain' => $domain,
|
||||
'registrar' => null,
|
||||
'registrar_url' => null,
|
||||
'expiration_date' => null,
|
||||
'updated_date' => null,
|
||||
'creation_date' => null,
|
||||
'abuse_email' => null,
|
||||
'nameservers' => [],
|
||||
'status' => [],
|
||||
'owner' => 'Unknown',
|
||||
'whois_server' => $rdapHost . ' (RDAP)',
|
||||
'raw_data' => []
|
||||
];
|
||||
|
||||
// Parse events (dates)
|
||||
if (isset($rdapData['events']) && is_array($rdapData['events'])) {
|
||||
foreach ($rdapData['events'] as $event) {
|
||||
$action = $event['eventAction'] ?? '';
|
||||
$date = $event['eventDate'] ?? '';
|
||||
|
||||
if (!empty($date)) {
|
||||
$parsedDate = date('Y-m-d', strtotime($date));
|
||||
|
||||
if ($action === 'registration') {
|
||||
$info['creation_date'] = $parsedDate;
|
||||
} elseif ($action === 'expiration') {
|
||||
$info['expiration_date'] = $parsedDate;
|
||||
} elseif ($action === 'last changed') {
|
||||
$info['updated_date'] = $parsedDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse status
|
||||
if (isset($rdapData['status']) && is_array($rdapData['status'])) {
|
||||
$info['status'] = $rdapData['status'];
|
||||
}
|
||||
|
||||
// Parse entities (registrar, abuse contact)
|
||||
if (isset($rdapData['entities']) && is_array($rdapData['entities'])) {
|
||||
foreach ($rdapData['entities'] as $entity) {
|
||||
$roles = $entity['roles'] ?? [];
|
||||
|
||||
// Registrar
|
||||
if (in_array('registrar', $roles)) {
|
||||
// Get registrar name from vCard
|
||||
if (isset($entity['vcardArray'][1])) {
|
||||
foreach ($entity['vcardArray'][1] as $vcardField) {
|
||||
if ($vcardField[0] === 'fn') {
|
||||
$info['registrar'] = $vcardField[3];
|
||||
} elseif ($vcardField[0] === 'url') {
|
||||
$info['registrar_url'] = $vcardField[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for abuse contact in nested entities
|
||||
if (isset($entity['entities']) && is_array($entity['entities'])) {
|
||||
foreach ($entity['entities'] as $subEntity) {
|
||||
if (in_array('abuse', $subEntity['roles'] ?? [])) {
|
||||
if (isset($subEntity['vcardArray'][1])) {
|
||||
foreach ($subEntity['vcardArray'][1] as $vcardField) {
|
||||
if ($vcardField[0] === 'email') {
|
||||
$info['abuse_email'] = $vcardField[3];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse nameservers
|
||||
if (isset($rdapData['nameservers']) && is_array($rdapData['nameservers'])) {
|
||||
foreach ($rdapData['nameservers'] as $ns) {
|
||||
$nsName = $ns['ldhName'] ?? '';
|
||||
if (!empty($nsName)) {
|
||||
// Remove trailing dot if present
|
||||
$nsName = rtrim($nsName, '.');
|
||||
$info['nameservers'][] = strtolower($nsName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set default registrar if not found
|
||||
if ($info['registrar'] === null) {
|
||||
$info['registrar'] = 'Unknown';
|
||||
}
|
||||
|
||||
$info['raw_data'] = [
|
||||
'states' => $info['status'],
|
||||
'nameServers' => $info['nameservers'],
|
||||
];
|
||||
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query WHOIS server
|
||||
*/
|
||||
private function queryWhois(string $domain, string $server, int $port = 43): ?string
|
||||
{
|
||||
$timeout = 10;
|
||||
|
||||
// Try to connect to WHOIS server
|
||||
$fp = @fsockopen($server, $port, $errno, $errstr, $timeout);
|
||||
|
||||
if (!$fp) {
|
||||
error_log("WHOIS connection failed to $server: $errstr ($errno)");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Send query
|
||||
fputs($fp, $domain . "\r\n");
|
||||
|
||||
// Get response
|
||||
$response = '';
|
||||
while (!feof($fp)) {
|
||||
$response .= fgets($fp, 128);
|
||||
}
|
||||
|
||||
fclose($fp);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse WHOIS data
|
||||
*/
|
||||
private function parseWhoisData(string $domain, string $whoisData, string $whoisServer = 'Unknown'): array
|
||||
{
|
||||
$lines = explode("\n", $whoisData);
|
||||
$data = [
|
||||
'domain' => $domain,
|
||||
'registrar' => null,
|
||||
'registrar_url' => null,
|
||||
'expiration_date' => null,
|
||||
'updated_date' => null,
|
||||
'creation_date' => null,
|
||||
'abuse_email' => null,
|
||||
'nameservers' => [],
|
||||
'status' => [],
|
||||
'owner' => 'Unknown',
|
||||
'whois_server' => $whoisServer,
|
||||
'raw_data' => []
|
||||
];
|
||||
|
||||
// Check if domain is not found/available
|
||||
$whoisDataLower = strtolower($whoisData);
|
||||
if (preg_match('/not found|no match|no entries found|no data found|domain not found|no such domain|not registered|available for registration/i', $whoisDataLower)) {
|
||||
$data['status'][] = 'AVAILABLE';
|
||||
$data['registrar'] = 'Not Registered';
|
||||
return $data;
|
||||
}
|
||||
|
||||
$registrarFound = false;
|
||||
$currentSection = null;
|
||||
|
||||
foreach ($lines as $index => $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (empty($line) || $line[0] === '%' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for section headers (UK format - lines ending with colon, no value)
|
||||
if (preg_match('/^([^:]+):\s*$/', $line, $matches)) {
|
||||
$currentSection = strtolower(trim($matches[1]));
|
||||
|
||||
// For UK domains: Registrar section - next line has the actual registrar
|
||||
if ($currentSection === 'registrar' && !$registrarFound && isset($lines[$index + 1])) {
|
||||
$nextLine = trim($lines[$index + 1]);
|
||||
if (!empty($nextLine)) {
|
||||
// Extract registrar name (remove [Tag = XXX] part)
|
||||
$registrarName = preg_replace('/\s*\[Tag\s*=\s*[^\]]+\]/i', '', $nextLine);
|
||||
$registrarName = trim($registrarName);
|
||||
if (!empty($registrarName)) {
|
||||
$data['registrar'] = $registrarName;
|
||||
$registrarFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// For multi-line sections (UK format), check if we're in a specific section
|
||||
if ($currentSection === 'name servers') {
|
||||
// Extract nameserver (format: "ns1.example.com 192.168.1.1")
|
||||
if (!preg_match('/^(This|--|\d+\.)/', $line)) {
|
||||
$ns = preg_split('/\s+/', $line)[0]; // Get first part (nameserver)
|
||||
if (!empty($ns) && strpos($ns, '.') !== false && !in_array(strtolower($ns), $data['nameservers'])) {
|
||||
$data['nameservers'][] = strtolower($ns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse key-value pairs
|
||||
if (strpos($line, ':') !== false) {
|
||||
list($key, $value) = explode(':', $line, 2);
|
||||
$key = trim(strtolower($key));
|
||||
$value = trim($value);
|
||||
|
||||
// For UK format - check for URL in registrar section
|
||||
if ($key === 'url' && $currentSection === 'registrar' && !empty($value)) {
|
||||
$data['registrar_url'] = $value;
|
||||
}
|
||||
|
||||
// Expiration date
|
||||
if (preg_match('/(expir|expiry|expire|paid-till|renewal)/i', $key) && !empty($value)) {
|
||||
$data['expiration_date'] = $this->parseDate($value);
|
||||
}
|
||||
|
||||
// Updated date (UK format: "Last updated")
|
||||
if (preg_match('/(updated date|last updated)/i', $key) && !empty($value)) {
|
||||
$data['updated_date'] = $this->parseDate($value);
|
||||
}
|
||||
|
||||
// Creation date (UK format: "Registered on")
|
||||
if (preg_match('/(creat|registered|registered on)/i', $key) && !empty($value)) {
|
||||
$data['creation_date'] = $this->parseDate($value);
|
||||
}
|
||||
|
||||
// Registrar (only take the first valid one found) - for standard format
|
||||
if (!$registrarFound && preg_match('/^registrar(?!.*url|.*whois|.*iana|.*phone|.*email|.*fax|.*abuse|.*id|.*contact)/i', $key) && !empty($value)) {
|
||||
// Skip if it looks like a phone number, email, or ID
|
||||
if (!preg_match('/^[\+\d\.\s\(\)-]+$/', $value) &&
|
||||
!preg_match('/@/', $value) &&
|
||||
!preg_match('/^\d+$/', $value) &&
|
||||
strlen($value) > 3) {
|
||||
$data['registrar'] = $value;
|
||||
$registrarFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Nameservers (standard format)
|
||||
if (preg_match('/(name server|nserver|nameserver)/i', $key) && !empty($value)) {
|
||||
$ns = preg_replace('/\s+.*$/', '', $value); // Remove IP addresses
|
||||
if (!empty($ns) && !in_array($ns, $data['nameservers'])) {
|
||||
$data['nameservers'][] = strtolower($ns);
|
||||
}
|
||||
}
|
||||
|
||||
// Status (UK format: "Registration status")
|
||||
if (preg_match('/(status|state|registration status)/i', $key) && !empty($value)) {
|
||||
if (!in_array($value, $data['status'])) {
|
||||
$data['status'][] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
// Registrar URL (standard format)
|
||||
if (preg_match('/^registrar url/i', $key) && !empty($value)) {
|
||||
$data['registrar_url'] = $value;
|
||||
}
|
||||
|
||||
// WHOIS Server
|
||||
if (preg_match('/registrar whois server/i', $key) && !empty($value)) {
|
||||
$data['whois_server'] = $value;
|
||||
}
|
||||
|
||||
// Abuse Email
|
||||
if (preg_match('/abuse.*email/i', $key) && !empty($value)) {
|
||||
$data['abuse_email'] = $value;
|
||||
}
|
||||
|
||||
// Owner/Registrant
|
||||
if (preg_match('/(registrant|owner)/i', $key) && !preg_match('/(email|phone|fax)/i', $key) && !empty($value)) {
|
||||
if ($data['owner'] === 'Unknown') {
|
||||
$data['owner'] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no registrar found, set default
|
||||
if ($data['registrar'] === null) {
|
||||
$data['registrar'] = 'Unknown';
|
||||
}
|
||||
|
||||
$data['raw_data'] = [
|
||||
'states' => $data['status'],
|
||||
'nameServers' => $data['nameservers'],
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse date from various formats
|
||||
*/
|
||||
private function parseDate(?string $dateString): ?string
|
||||
{
|
||||
if (empty($dateString)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove common prefixes/suffixes
|
||||
$dateString = preg_replace('/^(before|after):/i', '', $dateString);
|
||||
$dateString = trim($dateString);
|
||||
|
||||
// Try to parse the date
|
||||
$timestamp = strtotime($dateString);
|
||||
|
||||
if ($timestamp === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return date('Y-m-d', $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate days until domain expiration
|
||||
*/
|
||||
public function daysUntilExpiration(?string $expirationDate): ?int
|
||||
{
|
||||
if (!$expirationDate) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$expiration = strtotime($expirationDate);
|
||||
$now = time();
|
||||
$diff = $expiration - $now;
|
||||
|
||||
return (int)floor($diff / 86400); // 86400 seconds in a day
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain status based on expiration and WHOIS status
|
||||
*/
|
||||
public function getDomainStatus(?string $expirationDate, array $statusArray = []): string
|
||||
{
|
||||
// Check if domain is available (not registered)
|
||||
foreach ($statusArray as $status) {
|
||||
if (stripos($status, 'AVAILABLE') !== false || stripos($status, 'FREE') !== false) {
|
||||
return 'available';
|
||||
}
|
||||
}
|
||||
|
||||
$days = $this->daysUntilExpiration($expirationDate);
|
||||
|
||||
if ($days === null) {
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if ($days < 0) {
|
||||
return 'expired';
|
||||
}
|
||||
|
||||
if ($days <= 30) {
|
||||
return 'expiring_soon';
|
||||
}
|
||||
|
||||
return 'active';
|
||||
}
|
||||
|
||||
/**
|
||||
* Test domain status detection with a specific domain
|
||||
* This method is useful for debugging and testing
|
||||
*/
|
||||
public function testDomainStatus(string $domain): array
|
||||
{
|
||||
$info = $this->getDomainInfo($domain);
|
||||
|
||||
if (!$info) {
|
||||
return [
|
||||
'domain' => $domain,
|
||||
'status' => 'error',
|
||||
'message' => 'Failed to retrieve domain information'
|
||||
];
|
||||
}
|
||||
|
||||
$status = $this->getDomainStatus($info['expiration_date'], $info['status']);
|
||||
|
||||
return [
|
||||
'domain' => $domain,
|
||||
'status' => $status,
|
||||
'info' => $info,
|
||||
'message' => 'Domain status determined successfully'
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user