diff --git a/automation/config.php.dist b/automation/config.php.dist index 40233ab..0597911 100644 --- a/automation/config.php.dist +++ b/automation/config.php.dist @@ -80,4 +80,26 @@ return [ // Minimum Data Set 'minimum_data' => false, -]; \ No newline at end of file + + // Domain lifecycle settings + 'autoRenewEnabled' => false, + + // Lifecycle periods (in days) + 'gracePeriodDays' => 30, + 'autoRenewPeriodDays' => 45, + 'addPeriodDays' => 5, + 'renewPeriodDays' => 5, + 'transferPeriodDays' => 5, + 'redemptionPeriodDays' => 30, + 'pendingDeletePeriodDays' => 5, + + // Lifecycle phases (enable/disable) + 'enableAutoRenew' => false, + 'enableGracePeriod' => true, + 'enableRedemptionPeriod' => true, + 'enablePendingDelete' => true, + + // Drop settings + 'dropStrategy' => 'random', // Options: 'fixed', 'random' + 'dropTime' => '02:00:00', // Time of day to perform drops if 'fixed' strategy is used +]; diff --git a/automation/cron.php b/automation/cron.php index c1a7412..ced0572 100644 --- a/automation/cron.php +++ b/automation/cron.php @@ -19,7 +19,7 @@ $scheduler->php('/opt/registry/automation/statistics.php')->at('59 * * * *'); $scheduler->php('/opt/registry/automation/rdap-urls.php')->at('50 1 * * *'); $scheduler->php('/var/www/cp/bin/file_cache.php')->at('0 0 * * 1'); -$scheduler->php('/opt/registry/automation/change-domain-status.php')->at('*/5 * * * *'); +$scheduler->php('/opt/registry/automation/domain-lifecycle-manager.php')->at('*/5 * * * *'); $scheduler->php('/opt/registry/automation/auto-approve-transfer.php')->at('*/30 * * * *'); $scheduler->php('/opt/registry/automation/auto-clean-unused-contact-and-host.php')->at('5 0 * * *'); diff --git a/automation/domain-lifecycle-manager.php b/automation/domain-lifecycle-manager.php new file mode 100644 index 0000000..c2d042f --- /dev/null +++ b/automation/domain-lifecycle-manager.php @@ -0,0 +1,449 @@ +info('Job started.'); + +$lockFile = '/tmp/domain-lifecycle-manager.lock'; + +if (file_exists($lockFile)) { + $lockTime = filemtime($lockFile); + // Check if the lock file is stale (e.g., older than 10 minutes) + if (time() - $lockTime > 600) { + // Remove stale lock file + unlink($lockFile); + } else { + $log->warning('Script is already running. Exiting.'); + exit(0); + } +} + +// Create the lock file +touch($lockFile); + +try { + // Connect to the database + $dsn = "{$c['db_type']}:host={$c['db_host']};dbname={$c['db_database']};port={$c['db_port']}"; + $dbh = new PDO($dsn, $c['db_username'], $c['db_password']); + $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + // Instantiate the DomainLifecycleManager + $manager = new DomainLifecycleManager($dbh, $log, $c); + $manager->processLifecycle(); + + $log->info('Job finished successfully.'); +} catch (PDOException $e) { + $log->error('DB Connection failed: ' . $e->getMessage()); + exit(1); +} catch (Exception $e) { + $log->error('An unexpected error occurred: ' . $e->getMessage()); + exit(1); +} finally { + // Remove the lock file + unlink($lockFile); +} + +class DomainLifecycleManager { + private $dbh; + private $log; + private $config; + + public function __construct($dbh, $log, $config) { + $this->dbh = $dbh; + $this->log = $log; + $this->config = $config; + } + + public function processLifecycle() { + // Process lifecycle phases + if ($this->config['enableAutoRenew']) { + $this->processAutoRenewal(); + } else { + $this->processGracePeriod(); + } + + $this->cleanUpPeriods(); + + if ($this->config['enableRedemptionPeriod']) { + $this->processPendingDelete(); + $this->processPendingRestore(); + } + + if ($this->config['enablePendingDelete']) { + $this->processDomainDeletion(); + } + } + + // ======================== + // 1. Auto-Renewal Phase + // ======================== + private function processAutoRenewal() { + $this->log->info('Starting Auto-Renewal Phase.'); + + $minimum_data = $this->config['minimum_data']; + + // Fetch domains eligible for auto-renewal + $sth = $this->dbh->prepare(" + SELECT id, name, tldid, exdate, clid + FROM domain + WHERE CURRENT_TIMESTAMP > exdate + AND rgpstatus IS NULL + "); + $sth->execute(); + + while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { + $domain_id = $row['id']; + $name = $row['name']; + $tldid = $row['tldid']; + $exdate = $row['exdate']; + $clid = $row['clid']; + + // Check if domain can be set to auto-renew period + if ($this->canSetAutoRenewPeriod($domain_id)) { + // Process auto-renewal + $this->handleAutoRenewal($domain_id, $name, $tldid, $exdate, $clid); + } + + $this->log->info("$name (ID $domain_id) processed for auto-renewal."); + } + + $this->log->info('Completed Auto-Renewal Phase.'); + } + + private function canSetAutoRenewPeriod($domain_id) { + $sth_status = $this->dbh->prepare("SELECT status FROM domain_status WHERE domain_id = ?"); + $sth_status->execute([$domain_id]); + + while ($status_row = $sth_status->fetch(PDO::FETCH_ASSOC)) { + $status = $status_row['status']; + if (preg_match("/(serverUpdateProhibited|serverDeleteProhibited)$/", $status) || preg_match("/^pending/", $status)) { + return false; + } + } + return true; + } + + private function handleAutoRenewal($domain_id, $name, $tldid, $exdate, $clid) { + // Get registrar balance and credit limit + $sthRegistrar = $this->dbh->prepare(" + SELECT accountBalance, creditLimit + FROM registrar + WHERE id = ? + LIMIT 1 + "); + $sthRegistrar->execute([$clid]); + list($registrar_balance, $creditLimit) = $sthRegistrar->fetch(PDO::FETCH_NUM); + + // Get domain price + $returnValue = getDomainPrice($this->dbh, $name, $tldid, 12, 'renew', $clid); + $price = $returnValue['price']; + + if (($registrar_balance + $creditLimit) > $price) { + // Proceed with auto-renewal + $this->renewDomain($domain_id, $name, $exdate, $clid, $price); + } else { + // Insufficient funds, move to redemption period + $this->moveToRedemptionPeriod($domain_id, $exdate); + } + } + + private function renewDomain($domain_id, $name, $exdate, $clid, $price) { + // Update domain and registrar records + $sth = $this->dbh->prepare("UPDATE domain SET rgpstatus = 'autoRenewPeriod', exdate = DATE_ADD(exdate, INTERVAL 12 MONTH), autoRenewPeriod = '12', renewedDate = exdate WHERE id = ?"); + $sth->execute([$domain_id]); + + $sth = $this->dbh->prepare("UPDATE registrar SET accountBalance = (accountBalance - ?) WHERE id = ?"); + $sth->execute([$price, $clid]); + + $sth = $this->dbh->prepare("INSERT INTO payment_history (registrar_id, date, description, amount) VALUES(?, CURRENT_TIMESTAMP, ?, ?)"); + $description = "autoRenew domain $name for period 12 MONTH"; + $sth->execute([$clid, $description, -$price]); + + list($to) = $this->dbh->query("SELECT exdate FROM domain WHERE id = '$domain_id' LIMIT 1")->fetch(PDO::FETCH_NUM); + $sthStatement = $this->dbh->prepare("INSERT INTO statement (registrar_id, date, command, domain_name, length_in_months, fromS, toS, amount) VALUES(?, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?)"); + $sthStatement->execute([$clid, 'autoRenew', $name, '12', $exdate, $to, $price]); + + // Update statistics + $this->updateStatistics('renewed_domains'); + } + + private function moveToRedemptionPeriod($domain_id, $exdate) { + $this->dbh->beginTransaction(); + try { + $this->dbh->prepare("DELETE FROM domain_status WHERE domain_id = ?")->execute([$domain_id]); + $this->dbh->prepare("UPDATE domain SET rgpstatus = 'redemptionPeriod', delTime = ? WHERE id = ?")->execute([$exdate, $domain_id]); + $this->dbh->prepare("INSERT INTO domain_status (domain_id, status) VALUES(?, 'pendingDelete')")->execute([$domain_id]); + + $this->dbh->commit(); + } catch (Exception $e) { + $this->dbh->rollBack(); + $this->log->error("Failed to move domain ID $domain_id to redemption period: " . $e->getMessage()); + } + } + + // ======================== + // 2. Grace Period Phase + // ======================== + private function processGracePeriod() { + $this->log->info('Starting Grace Period Phase.'); + + $gracePeriodDays = $this->config['gracePeriodDays']; + + // Fetch domains eligible for grace period + $sth = $this->dbh->prepare(" + SELECT id, name, exdate + FROM domain + WHERE CURRENT_TIMESTAMP > DATE_ADD(exdate, INTERVAL ? DAY) + AND rgpstatus IS NULL + "); + $sth->execute([$gracePeriodDays]); + + while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { + $domain_id = $row['id']; + $name = $row['name']; + $exdate = $row['exdate']; + + // Check if domain can be moved to redemption period + if ($this->canSetGracePeriod($domain_id)) { + $this->moveToRedemptionPeriod($domain_id, $exdate); + $this->log->info("$name (ID $domain_id) moved to redemption period."); + } + } + + $this->log->info('Completed Grace Period Phase.'); + } + + private function canSetGracePeriod($domain_id) { + $sth_status = $this->dbh->prepare("SELECT status FROM domain_status WHERE domain_id = ?"); + $sth_status->execute([$domain_id]); + + while ($status_row = $sth_status->fetch(PDO::FETCH_ASSOC)) { + $status = $status_row['status']; + if (preg_match("/(serverUpdateProhibited|serverDeleteProhibited)$/", $status) || preg_match("/^pending/", $status)) { + return false; + } + } + return true; + } + + // ======================== + // 3. Clean-Up Periods + // ======================== + private function cleanUpPeriods() { + $this->log->info('Starting Clean-Up Periods Phase.'); + + $periods = [ + 'autoRenewPeriod' => $this->config['autoRenewPeriodDays'], + 'addPeriod' => $this->config['addPeriodDays'], + 'renewPeriod' => $this->config['renewPeriodDays'], + 'transferPeriod' => $this->config['transferPeriodDays'], + ]; + + foreach ($periods as $periodName => $days) { + $this->cleanupPeriod($periodName, $days); + } + + $this->log->info('Completed Clean-Up Periods Phase.'); + } + + private function cleanupPeriod($periodName, $days) { + $column = $periodName === 'addPeriod' ? 'crdate' : ($periodName === 'renewPeriod' ? 'renewedDate' : ($periodName === 'transferPeriod' ? 'trdate' : 'exdate')); + $sth = $this->dbh->prepare("UPDATE domain SET rgpstatus = NULL WHERE CURRENT_TIMESTAMP > DATE_ADD($column, INTERVAL ? DAY) AND rgpstatus = ?"); + $sth->execute([$days, $periodName]); + } + + // ======================== + // 4. Pending Delete Phase + // ======================== + private function processPendingDelete() { + $this->log->info('Starting Pending Delete Phase.'); + + $redemptionPeriodDays = $this->config['redemptionPeriodDays']; + + // Fetch domains eligible for pending delete + $sth = $this->dbh->prepare(" + SELECT id, name, exdate + FROM domain + WHERE CURRENT_TIMESTAMP > DATE_ADD(delTime, INTERVAL ? DAY) + AND rgpstatus = 'redemptionPeriod' + "); + $sth->execute([$redemptionPeriodDays]); + + while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { + $domain_id = $row['id']; + $name = $row['name']; + + if ($this->canSetPendingDelete($domain_id)) { + $sthUpdate = $this->dbh->prepare("UPDATE domain SET rgpstatus = 'pendingDelete' WHERE id = ?"); + $sthUpdate->execute([$domain_id]); + $this->log->info("$name (ID $domain_id) moved to pending delete."); + } + } + + $this->log->info('Completed Pending Delete Phase.'); + } + + private function canSetPendingDelete($domain_id) { + $sth_status = $this->dbh->prepare("SELECT status FROM domain_status WHERE domain_id = ?"); + $sth_status->execute([$domain_id]); + + while ($status_row = $sth_status->fetch(PDO::FETCH_ASSOC)) { + $status = $status_row['status']; + if (preg_match("/(serverUpdateProhibited|serverDeleteProhibited)$/", $status)) { + return false; + } + } + return true; + } + + // ======================== + // 5. Pending Restore Phase + // ======================== + private function processPendingRestore() { + $this->log->info('Starting Pending Restore Phase.'); + + // Fetch domains in pendingRestore status that have exceeded the restore period + $sth = $this->dbh->prepare(" + SELECT id, name + FROM domain + WHERE rgpstatus = 'pendingRestore' + AND CURRENT_TIMESTAMP > DATE_ADD(resTime, INTERVAL 7 DAY) + "); + $sth->execute(); + + while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { + $domain_id = $row['id']; + $name = $row['name']; + + $sthUpdate = $this->dbh->prepare("UPDATE domain SET rgpstatus = 'redemptionPeriod' WHERE id = ?"); + $sthUpdate->execute([$domain_id]); + + $this->log->info("$name (ID $domain_id) returned to redemption period from pending restore."); + } + + $this->log->info('Completed Pending Restore Phase.'); + } + + // ======================== + // 6. Domain Deletion Phase + // ======================== + private function processDomainDeletion() { + $this->log->info('Starting Domain Deletion Phase.'); + + $totalPendingDays = $this->config['redemptionPeriodDays'] + $this->config['pendingDeletePeriodDays']; + + // Fetch domains eligible for deletion + $sth = $this->dbh->prepare(" + SELECT id, name, delTime + FROM domain + WHERE CURRENT_TIMESTAMP > DATE_ADD(delTime, INTERVAL ? DAY) + AND rgpstatus = 'pendingDelete' + "); + $sth->execute([$totalPendingDays]); + + while ($row = $sth->fetch(PDO::FETCH_ASSOC)) { + $domain_id = $row['id']; + $name = $row['name']; + $delTime = $row['delTime']; + + if ($this->shouldDeleteDomainNow($delTime)) { + if ($this->canDeleteDomain($domain_id)) { + $this->deleteDomain($domain_id, $name); + $this->log->info("$name (ID $domain_id) has been deleted."); + } + } + } + + $this->log->info('Completed Domain Deletion Phase.'); + } + + private function shouldDeleteDomainNow($delTime) { + $currentTime = new DateTime(); + $deletionTime = new DateTime($delTime); + $totalPendingDays = $this->config['redemptionPeriodDays'] + $this->config['pendingDeletePeriodDays']; + $deletionTime->modify("+$totalPendingDays days"); + + if ($this->config['dropStrategy'] === 'fixed') { + $dropTime = $this->config['dropTime']; + $deletionTime->setTime((int)substr($dropTime, 0, 2), (int)substr($dropTime, 3, 2), (int)substr($dropTime, 6, 2)); + return $currentTime >= $deletionTime; + } elseif ($this->config['dropStrategy'] === 'random') { + $randomDays = rand(0, $this->config['pendingDeletePeriodDays']); + $deletionTime->modify("+$randomDays days"); + return $currentTime >= $deletionTime; + } else { + return $currentTime >= $deletionTime; + } + } + + private function canDeleteDomain($domain_id) { + $sth_status = $this->dbh->prepare("SELECT status FROM domain_status WHERE domain_id = ?"); + $sth_status->execute([$domain_id]); + + $delete_domain = false; + while ($status_row = $sth_status->fetch(PDO::FETCH_ASSOC)) { + $status = $status_row['status']; + if ($status == 'pendingDelete') { + $delete_domain = true; + } + if (preg_match("/(serverUpdateProhibited|serverDeleteProhibited)$/", $status)) { + return false; + } + } + return $delete_domain; + } + + private function deleteDomain($domain_id, $name) { + $minimum_data = $this->config['minimum_data']; + + $this->dbh->beginTransaction(); + try { + // Delete associated hosts + $sth = $this->dbh->prepare("SELECT id FROM host WHERE domain_id = ?"); + $sth->execute([$domain_id]); + while ($host_row = $sth->fetch(PDO::FETCH_ASSOC)) { + $host_id = $host_row['id']; + $this->dbh->prepare("DELETE FROM host_addr WHERE host_id = ?")->execute([$host_id]); + $this->dbh->prepare("DELETE FROM host_status WHERE host_id = ?")->execute([$host_id]); + $this->dbh->prepare("DELETE FROM domain_host_map WHERE host_id = ?")->execute([$host_id]); + } + + if (!$minimum_data) { + $this->dbh->prepare("DELETE FROM domain_contact_map WHERE domain_id = ?")->execute([$domain_id]); + } + $this->dbh->prepare("DELETE FROM domain_host_map WHERE domain_id = ?")->execute([$domain_id]); + $this->dbh->prepare("DELETE FROM domain_authInfo WHERE domain_id = ?")->execute([$domain_id]); + $this->dbh->prepare("DELETE FROM domain_status WHERE domain_id = ?")->execute([$domain_id]); + $this->dbh->prepare("DELETE FROM host WHERE domain_id = ?")->execute([$domain_id]); + + $this->dbh->prepare("DELETE FROM domain WHERE id = ?")->execute([$domain_id]); + + // Update statistics + $this->updateStatistics('deleted_domains'); + + $this->dbh->commit(); + } catch (Exception $e) { + $this->dbh->rollBack(); + $this->log->error("$domain_id|$name could not be deleted: " . $e->getMessage()); + } + } + + // ======================== + // Helper Methods + // ======================== + private function updateStatistics($field) { + // Ensure today's statistics record exists + $sth = $this->dbh->prepare("SELECT id FROM statistics WHERE date = CURDATE()"); + $sth->execute(); + if (!$sth->fetchColumn()) { + $this->dbh->prepare("INSERT INTO statistics (date) VALUES (CURDATE())")->execute(); + } + // Update the specific field + $sthUpdate = $this->dbh->prepare("UPDATE statistics SET $field = $field + 1 WHERE date = CURDATE()"); + $sthUpdate->execute(); + } +} diff --git a/docs/update108.sh b/docs/update108.sh index b99bf96..1f2c680 100644 --- a/docs/update108.sh +++ b/docs/update108.sh @@ -125,6 +125,37 @@ if [ -f "$CONFIG_FILE" ]; then sed -i "/'dns_serial' => 1, \/\/ change to 2 for YYYYMMDDXX format, and 3 for Cloudflare-like serial/{n;/^$/d}" "$CONFIG_FILE" fi +CONFIG_FILE="/opt/registry/automation/config.php" +if [ -f "$CONFIG_FILE" ]; then + # Add 'minimum_data' with an empty line after it + sed -i "/'minimum_data'/a\\ +\\ +// Domain lifecycle settings\\ +'autoRenewEnabled' => false,\\ +\\ +// Lifecycle periods (in days)\\ +'gracePeriodDays' => 30,\\ +'autoRenewPeriodDays' => 45,\\ +'addPeriodDays' => 5,\\ +'renewPeriodDays' => 5,\\ +'transferPeriodDays' => 5,\\ +'redemptionPeriodDays' => 30,\\ +'pendingDeletePeriodDays' => 5,\\ +\\ +// Lifecycle phases (enable/disable)\\ +'enableAutoRenew' => false,\\ +'enableGracePeriod' => true,\\ +'enableRedemptionPeriod' => true,\\ +'enablePendingDelete' => true,\\ +\\ +// Drop settings\\ +'dropStrategy' => 'random', // Options: 'fixed', 'random'\\ +'dropTime' => '02:00:00', // Time of day to perform drops if 'fixed' strategy is used" "$CONFIG_FILE" + + # Remove any extra blank lines added by accident after the block + sed -i '/^$/N;/^\n$/D' "$CONFIG_FILE" +fi + # Start services echo "Starting services..." systemctl start epp