<?php
###############################################################################
# RDAP.php
#
# @author Anil Kumar <akumar@codepunch.com>
# @link   https://codepunch.com
#
###############################################################################

namespace 	CodePunch\LU;
use 		CodePunch\Base\Util as UTIL;
use			CodePunch\Base\Text as TEXT;

###############################################################################

class RDAP  {
	
	private $lookupManager = null;
	
	###########################################################################
	
	public function __construct($lum=null) {
		$this->lookupManager = $lum;
	}
	
	###########################################################################
	
	public function registryRDAP(&$ludata) 
	{
		$lookupcount = 0;
		$did = $ludata['sid'];
		$db = $this->lookupManager->getAuthentication()->getDatabase();
		$domain = $db->getDomainName($did);
		$ludata['api_profile'] = $db->findOneOf($db->getDomainTableName(), "sid", $did, "api_profile");
		if($domain !== false) {
			$server = $ludata['server'];
			if($server != "" && $server != "unknown") {
				if($this->lookupManager->isConnectionsAllowedToServer($server)) {
					$ext_proxy = $this->lookupManager->getProxy('rdap');
					$dataarray = self::lookup($domain, $server, 0, $ext_proxy);
					
					// June 2025
					if(isset($dataarray['wrong']) && $dataarray['wrong']) {
						$logger = new \CodePunch\Base\CPLogger();
						$emsg = TEXT::get("registrar_rdap_wrong_domain_return_bug") . ": $server";
						UTIL::print($emsg);
						$logger->error($emsg);
						$ludata['status'] = \CodePunch\LU\LookupManager::LUQ_FAIL;
						return $lookupcount;
					}
					// End June 2025
					
					$dataarray = self::normalize($dataarray, 0);
					$this->checkForThrottling($server, $dataarray);
					$lookupcount++;
					$this->lookupManager->setLastConnectForServer($server);
					if(is_array($dataarray))
						$this->lookupManager->resetDomainData($domain);
					$dataarray['sid'] = $did;
					$dataarray['primary_rdap_server'] = $server;
					$dataarray['primary_whois_checked_at'] = date("Y-m-d H:i:s");
					$ludata['registrar_abuse_email'] = UTIL::get_from_array($dataarray['registrar_abuse_email'], "");
					$this->lookupManager->updateDomainTable(\CodePunch\LU\LookupManager::DOMAIN_RECORDS, $dataarray);
					if((isset($dataarray['secondary_rdap_server']) && $dataarray['secondary_rdap_server'] != "") || ($ludata['api_profile'] != "" && $ludata['api_profile'] !== false)) {
						$ludata['lutype'] = \CodePunch\LU\LookupManager::AUTH_DOMAIN_RECORDS;
						$lookupcount += $this->registrarRDAP($ludata);
					}
					else {
						if(isset($ludata['registrar_abuse_email'])) 
							self::guessSecondaryWhoisServer($db, $did, $ludata['registrar_abuse_email']);
						$ludata['lutype'] = \CodePunch\LU\LookupManager::AUTH_DOMAIN_RECORDS;
					}
					return $lookupcount;
				}
			}
			else {
				$logger = new \CodePunch\Base\CPLogger();
				$logger->error(TEXT::get("luq_whoissetup_error") . " [$domain]");
				$dataarray['primary_whois_checked_at'] = date("Y-m-d H:i:s");
				$dataarray['domain'] = $domain;
				$dataarray['registry_whois'] = "Error: " . TEXT::get("luq_whoissetup_error") . " [$domain]";
				$this->lookupManager->updateDomainTable(\CodePunch\LU\LookupManager::DOMAIN_RECORDS, $dataarray);
				$ludata['status'] = \CodePunch\LU\LookupManager::LUQ_UNSUPPORTED;
			}
		}
		else {
			$logger = new \CodePunch\Base\CPLogger();
			$logger->error(TEXT::get("luq_missingdomain"));
			$ludata['status'] = \CodePunch\LU\LookupManager::LUQ_UNSUPPORTED;
		}
		return $lookupcount;
	}
	
	###########################################################################
	// Will lookup the registrar RDAP server

	public function registrarRDAP(&$ludata)
	{
		
		$did = $ludata['sid'];
		$db = $this->lookupManager->getAuthentication()->getDatabase();
		$domain = $db->getDomainName($did);
		if($domain !== false) {
			if(isset($ludata['api_profile'])) {
				$secondary_whois_server = $db->findOneOf($db->getDomainTableName(), "sid", $did, "secondary_whois_server");
				if($ludata['api_profile'] !== false && $ludata['api_profile'] != "") {
					$whois02 = $this->lookupManager->getWhoisFromRegistrar($domain, $ludata['api_profile']);
					if($whois02 !== false) {
						$parser = new \CodePunch\LU\WhoisParser($db, $whois02, $secondary_whois_server);
						$data_array02 = $parser->processRegistrarData($domain);
						if(is_array($data_array02))
						{
							if(function_exists("sedf_after_registrar_whois_check"))
								sedf_after_registrar_whois_check($domain, $secondary_whois_server, $whois02, $data_array02, $spit_out);
							$data_array02['sid'] = $did;
							$this->lookupManager->updateDomainTable(\CodePunch\LU\LookupManager::AUTH_DOMAIN_RECORDS, $data_array02);
							$ludata['status'] = \CodePunch\LU\LookupManager::LUQ_COMPLETE;
							return 1;
						}
					}
				}
			}
			$secondary_rdap_server = $db->findOneOf($db->getDomainTableName(), "sid", $did, "secondary_rdap_server");
			if($secondary_rdap_server !== false && $secondary_rdap_server != "") {
				if($this->lookupManager->isConnectionsAllowedToServer($secondary_rdap_server)) {
					$ext_proxy = $this->lookupManager->getProxy('rdap');
					$dataarray = self::lookup($domain, $secondary_rdap_server, 1, $ext_proxy);
					$dataarray = self::normalize($dataarray, 1);
					$this->checkForThrottling($secondary_rdap_server, $dataarray);
					$this->lookupManager->setLastConnectForServer($secondary_rdap_server);
					if(is_array($dataarray) && $dataarray['httpcode'] == 200)
					{
						$dataarray['sid'] = $did;
						$dataarray['secondary_whois_checked_at'] = date("Y-m-d H:i:s");
						$this->lookupManager->updateDomainTable(\CodePunch\LU\LookupManager::AUTH_DOMAIN_RECORDS, $dataarray);
						$ludata['status'] = \CodePunch\LU\LookupManager::LUQ_COMPLETE;
						return 1;
					}
					else
					{
						// Mark secondary RDAP server as non working
						$db->updateDomainTable(array('sid'=>$did, 'secondary_rdap_server'=>"!".$secondary_rdap_server));
						// Let LookupManager attempt Normal port 43 Whois.
						// We find registrar domain and try "whois" host at that domain
						if(isset($ludata['registrar_abuse_email'])) 
							self::guessSecondaryWhoisServer($db, $did, $ludata['registrar_abuse_email']);
					}
				}
				else {
					// Busy, The LU will get queued again. 
				}
			}
		}
		return 0;
	}
	
	###########################################################################
	
	public function checkForThrottling($server, $data)
	{
		if($data['httpcode'] == 429) {
			// Throttled! Set the stop_lookups flag in connections_table
			$db = $this->lookupManager->getAuthentication()->getDatabase();
			$table = $db->getConnectionsTableName();
			$client = $this->lookupManager->getAuthentication()->getServerID();
			$db->updateTable($table, array('stop_lookups'=>true), "client=? AND server=?", array($client, $server));
			// TODO: Set the stop_interval too based on 'Retry-After' data
		}
	}
	
	###########################################################################
	
	public static function guessSecondaryWhoisServer($db, $did, $registrar_abuse_email)
	{
		$secondary_whois_server = $db->findOneOf($db->getDomainTableName(), "sid", $did, "secondary_whois_server");
		if($secondary_whois_server == false || $secondary_whois_server == "") {
			if($registrar_abuse_email != "") {
				$pos = strpos($registrar_abuse_email, "@");
				if($pos !== false) {
					$regdomain = substr($registrar_abuse_email, $pos+1);
					$whoisserver = "whois." . $regdomain;
					if($regdomain != 'godaddy.com' && \CodePunch\LU\Whois::hasWhoisPort($whoisserver, 2))
						$db->updateDomainTable(array('sid'=>$did, 'secondary_whois_server'=>$whoisserver));
				}
			}
		}
	}

	###########################################################################

	public static function fill($db)
	{
		$count = 0;
		$data = self::findAll();
		if($db) {
			foreach($data as $d) {
				$oldcount = $count;
				$tld = $d['tld'];
				unset($d['tld']);
				if($db->hasRow($db->getTLDsTableName(), "tld", $tld)) {
					if($db->updateTable($db->getTLDsTableName(), $d, "tld=?", $tld))
						$count++;
				}
				else {
					$d['tld'] = $tld;
					$d['server'] = '-';
					if($db->insertIntoTable($db->getTLDsTableName(), $d)) 
						$count++;
				}
				if($oldcount != $count)
					UTIL::debug_cli_print("$count] $tld" . ": " . $d['rdap_server']);
			}
		}
		if($count && $db)
			\CodePunch\DB\Audit::add($db, \CodePunch\DB\Audit::GENERIC_ACTION, "RDAP Servers ($count) filled in TLD Table", "");
		return $count;
	}

	###########################################################################

	public static function findForTLD($tld)
	{
		$tld = UTIL::idn_convert($tld);
		$rdapserver = "";
		$url = "https://data.iana.org/rdap/dns.json";
		$idata = UTIL::curl_get_url($url, $timeout=10, false, false, "-");
		if($idata['status'] == 200) {
			$data = $idata['result'];
			$data = (array)json_decode($data);
			if(isset($data['services'])) {
				$data = $data['services'];
				foreach($data as $d) {
					if(count($d) == 2) {
						$tlds = $d[0];
						$servers = $d[1];
						if(in_array($tld, $tlds)) {
							if(is_array($servers))
								$rdapserver = $servers[0];
							else
								$rdapserver = $servers;
							break;
						}
					}
				}
			}
		}
		return $rdapserver;
	}
	
	###########################################################################

	public static function findAll()
	{
		$tinfo = array();
		$url = "https://data.iana.org/rdap/dns.json";
		$idata = UTIL::curl_get_url($url, $timeout=10, false, false, "-");
		if($idata['status'] == 200) {
			$data = $idata['result'];
			$data = (array)json_decode($data);
			if(isset($data['services'])) {
				$data = $data['services'];
				foreach($data as $d) {
					if(count($d) == 2) {
						$tlds = $d[0];
						$servers = $d[1];
						if(is_array($servers))
							$rdapserver = $servers[0];
						else
							$rdapserver = $servers;
						foreach($tlds as $tld) {
							if(strtolower(substr($tld, 0, 4)) == "xn--")
								$tld = UTIL::idn_reconvert($tld);
							$tinfo[] = array('tld'=>$tld, 'rdap_server'=>$rdapserver);
						}
					}
				}
			}
		}
		return $tinfo;
	}
	
	###########################################################################
	
	public static function lookup($domain, $rdapserver, $mode, $ext_proxy=null)
	{
		$data_array = array();
		if($rdapserver != "") {
			$asciidomain = UTIL::idn_convert($domain);
			$url = trim($rdapserver, " \n\r\t/") . "/domain/$asciidomain";
			$data_array = self::lookupURL($domain, $url, $mode, $ext_proxy);
			if(!isset($data_array['domain']))
				$data_array['domain'] = $domain;
		}
		return $data_array;
	}
	
	###########################################################################
	
	public static function lookupURL($domain, $url, $mode, $ext_proxy=null)
	{
		$data_array = array();
		$idata = UTIL::curl_get_url($url, $timeout=10, false, false, "-", $ext_proxy, true);
		if($idata['status'] == 200) {
			$data = $idata['result'];
			$data = (array)json_decode($data, true);
			$data_array = self::parse($domain, $data, $mode);
			
			// June 2025
			if(isset($data_array['wrong']) && $data_array['wrong']) {
				UTIL::print("$domain: $url: $mode");
			}
			// End June 2025
			
			$data_array['raw'] = json_encode($data, JSON_PRETTY_PRINT);
		}
		else {
			if($idata['result'] != "") {
				$result = $idata['result'];
				$result = (array)json_decode($result, true);
			}
			else
				$result = array();
			if($idata['status'] != 0)
				$result['http_status_code'] = $idata['status'];
			if(isset($idata['error'])) {
				$result['rdap_error'] = $idata['error'];
				if(UTIL::starts_with($result['rdap_error'], "error:"))
					$result['rdap_error'] = trim(substr($result['rdap_error'], 6));
			}
			$result = json_encode($result, JSON_PRETTY_PRINT);
			$data_array['raw'] = $result;
		}
		$data_array['httpcode'] = $idata['status'];
		return $data_array;
	}
	
	###########################################################################
	
	public static function reParse($domain, $data, $mode)
	{
		$data = (array)json_decode($data, true);
		$data_array = self::parse($domain, $data, $mode);
		$data_array['raw'] = json_encode($data, JSON_PRETTY_PRINT);
		$data_array = self::normalize($data_array, $mode);
		return $data_array;
	}
	
	###########################################################################
	
	public static function normalize(&$data, $mode=0)
	{
		if(isset($data['registrant'])) {
			$data['owner'] = UTIL::get_from_array($data['registrant']['name'], "");
			$data['organization'] = UTIL::get_from_array($data['registrant']['organization'], "");
			$data['address'] = UTIL::get_from_array($data['registrant']['address'], "");
			$data['owner_country'] = UTIL::get_from_array($data['registrant']['country'], "");
			$data['registrant_email'] = UTIL::get_from_array($data['registrant']['email'], "");
			$data['registrant_phone'] = UTIL::get_from_array($data['registrant']['phone'], "");
			unset($data['registrant']);
		}
		if(isset($data['administrative'])) {
			$data['admin_email'] = UTIL::get_from_array($data['administrative']['email'], "");
			$data['admin_phone'] = UTIL::get_from_array($data['administrative']['phone'], "");
			unset($data['administrative']);
		}
		if(isset($data['technical'])) {
			$data['tech_email'] = UTIL::get_from_array($data['technical']['email'], "");
			$data['tech_phone'] = UTIL::get_from_array($data['technical']['phone'], "");
			unset($data['technical']);
		}
		if(isset($data['registrar'])) {
			if(isset($data['registrar']['fullname']))
				$registrar = UTIL::get_from_array($data['registrar']['fullname'], "");
			else if(isset($data['registrar']['organization']))
				$registrar = UTIL::get_from_array($data['registrar']['organization'], "");
			unset($data['registrar']);
			$data['registrar'] = $registrar;
		}
		if(isset($data['registrar_abuse'])) {
			$data['registrar_abuse_email'] = UTIL::get_from_array($data['registrar_abuse']['email'], "");
			unset($data['registrar_abuse']);
		}
		if(isset($data['last update of RDAP database'])) {
			$data['rdap_update'] = UTIL::get_from_array($data['last update of RDAP database'], "");
			unset($data['last update of RDAP database']);
		}
		if(isset($data['httpcode']) && !$mode) {
			if($data['httpcode'] == 200) {
				$data['availability'] = 'Not Available';
			}
			else if($data['httpcode'] == 404) {
				$data['availability'] = 'Available';
				$data['status'] = 'Not Registered';
			}
			else {
				$data['availability'] = 'Setup Error';
			}
		}
		
		if(isset($data['raw'])) {
			$rawdata = $data['raw'];
			unset($data['raw']);
			$wkey = $mode ? "registrar_whois" : "registry_whois";
			$data[$wkey] = $rawdata;
		}
		return $data;
	}
	
	###########################################################################
	
	public static function parse($domain, $data, $mode=0)
	{
		$data_array = array();
		
		if(isset($data['unicodeName']))
			$data_array['domain'] = $data['unicodeName'];
		
		// June 2025
		else if(isset($data['ldhName'])) {
			if(strcasecmp($domain, $data['ldhName'])) {
				// Not the domain we are expecting
				// Some registrar RDAP servers are buggy!
				// eg.: PublicDomainRegistry
				return ['wrong'=>true];
			}
			else 
				$data_array['domain'] = $data['ldhName'];
		}
		// End June 2025
		
		if(isset($data['links'])) {
			$links = $data['links'];
			foreach($links as $link) {
				if(isset($link['rel']) && $link['rel'] == 'related' && isset($link['type']) &&  $link['type'] == 'application/rdap+json') {
					$asciidomain = UTIL::idn_convert($domain);
					$secserver = str_ireplace("/domain/$domain", "/", $link['href']);
					$secserver = str_ireplace("/domain/$asciidomain", "/", $secserver);
					$data_array['secondary_rdap_server'] = $secserver;
				}
			}
		}
		if(isset($data['events'])) {
			$edata = (array)$data['events'];
			foreach($edata as $ed) {
				$key = UTIL::get_from_array($ed['eventAction'], "");
				$value = UTIL::get_from_array($ed['eventDate'], "");
				if($key == "expiration")
					$key = $mode ? "registrar_expiry" : "registry_expiry";
				else if($key == "registrar expiration")
					$key = "registrar_expiry";
				else if($key == "registration")
					$key = "created_on";
				else if($key == "last changed" || $key == "last update")
					$key = "last_update";
				if($key != "")
					$data_array[$key] = \CodePunch\DB\DomainDB::extractDateString($value, true);
			}
		}
		
		if(isset($data['nameServers']) || isset($data['nameservers'])) {
			$nameservers = array();
			$edata = isset($data['nameservers']) ? (array)$data['nameservers'] : (array)$data['nameServers'];
			foreach($edata as $ed) {
				$key = UTIL::get_from_array($ed['objectClassName'], "");
				$value = UTIL::get_from_array($ed['ldhName'], "");
				if(is_array($value) && isset($value['stringValue']))
					$value = $value['stringValue'];
				if($key != "")
					$nameservers[] = rtrim($value, '.');
			}
			if(is_array($nameservers))
				sort($nameservers);
			$idx = 1;
			foreach($nameservers as $ns) {
				$key = "ns$idx";
				$data_array[$key] = $ns;
				$idx++;
			}
		}
		
		if(isset($data['secureDNS'])) {
			$edata = (array)$data['secureDNS'];
			foreach($edata as $key=>$value) {
				$key = 'secure_dns_' . strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $key));
				if(is_array($value)) {
					if($key == 'secure_dns_ds_data' && count($value)) {
						$value = $value[0];
						foreach($value as $k=>$v) {
							$k = strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $k));
							if(is_array($v)) {
							}
							else
								$data_array[$key.'_'.$k] = $v;
						}
					}
				}
				else {
					$data_array[$key] = $value;
				}
			}
		}
		if(isset($data_array['secure_dns_delegation_signed']) && UTIL::str_to_bool($data_array['secure_dns_delegation_signed'])) {
			$data_array['dnssec_status'] = 'signedDelegation';
		}
		
		if(isset($data['status'])) {
			$status = array();
			$edata = (array)$data['status'];
			foreach($edata as $ed) {
				$status[] = $ed;
			}
			$status = array_map(function($string) { return str_replace(" ", "", $string); }, $status);
			$data_array['status'] = implode(", ", $status);
		}
		
		if(isset($data['entities'])) {
			$edata = (array)$data['entities'];
			foreach($edata as $ed) {
				if(isset($ed['roles'])) {
					$eroles = $ed['roles'];
					if(in_array('registrar', $eroles)) {
						$data_array['registrar_handle'] = UTIL::get_from_array($ed['handle'], "");
						if(isset($ed['vcardArray'])) {
							$vcard = $ed['vcardArray'][0];
							if(!is_array($vcard))
								$vcard = $ed['vcardArray'];
							$data_array['registrar'] = self::parsevCard($vcard);
						}
						
						// Find registrar abuse contacts for registry lookup
						if(!$mode) {
							$regdata = UTIL::get_from_array($ed['entities'], array());
							foreach($regdata as $erd) {
								$erroles = $erd['roles'];
								if(in_array('abuse', $erroles)) {
									if(isset($erd['vcardArray'])) {
										$vcard = $erd['vcardArray'][0];
										if(!is_array($vcard) && $vcard == "vcard")
											$vcard = $erd['vcardArray'];
										$data_array['registrar_abuse'] = self::parsevCard($vcard);
										break;
									}
								}
							}
						}
					}
					if(in_array('registrant', $eroles)) {
						if(isset($ed['vcardArray'])) {
							$vcard = $ed['vcardArray'][0];
							if(!is_array($vcard) && $vcard == "vcard")
								$vcard = $ed['vcardArray'];
							$data_array['registrant'] = self::parsevCard($vcard);
						}
					}
					if(in_array('administrative', $eroles)) {
						if(isset($ed['vcardArray'])) {
							$vcard = $ed['vcardArray'][0];
							if(!is_array($vcard) && $vcard == "vcard")
								$vcard = $ed['vcardArray'];
							$data_array['administrative'] = self::parsevCard($vcard);
						}
					}
					if(in_array('technical', $eroles)) {
						if(isset($ed['vcardArray'])) {
							$vcard = $ed['vcardArray'][0];
							if(!is_array($vcard) && $vcard == "vcard")
								$vcard = $ed['vcardArray'];
							$data_array['technical'] = self::parsevCard($vcard);
						}
					}
				}
			}
		}
		return $data_array;
	}
	
	###########################################################################
	
	public static function parsevCard($vcard)
	{
		$vi = array();
		if(is_array($vcard)) {
			foreach($vcard as $vc) {
				if(is_array($vc)) {
					if(count($vc) != 4) 
						return self::parsevCard($vc);
					$v = $vc[0];
					if(is_array($v))
						return self::parsevCard($vc);
					if(count($vc) == 4) {
						if($v == "adr") {
							if(isset($vc[3][2]) && is_array($vc[3][2])) {
								$vc[3][2] = implode(", ",array_filter($vc[3][2]));
							}
							$vi['address'] = implode(", ",array_filter($vc[3]));
							$vi['state'] = UTIL::get_from_array($vc[3][4], "");
							$vi['zip'] = UTIL::get_from_array($vc[3][5], "");
							$vi['country'] = UTIL::get_from_array($vc[3][6], "");
						}
						if($v == "email")
							$vi['email'] = $vc[3];
						if($v == "org")
							$vi['organization'] = $vc[3];
						if($v == "n")
							$vi['name'] = implode(" ", array_filter($vc[3]));
						if($v == "tel")
							$vi['phone'] = $vc[3];
						if($v == "fn")
							$vi['fullname'] = $vc[3];
					}
				}
			}
		}
		return $vi;
	}
}

###############################################################################
