4000000)return ''; $bg=self::mail_background(); $name='mail-logo-'.substr(md5($real.'|'.filesize($real).'|'.filemtime($real).'|'.$bg),0,16).'.png'; $dir=$up['basedir'].'/storeruler-mail';$out=$dir.'/'.$name;$link=untrailingslashit($up['baseurl']).'/storeruler-mail/'.$name; if(is_file($out))return $link; $rgb=array_map('hexdec',str_split(substr($bg,1),2)); if(!function_exists('imagecreatetruecolor'))return ''; $src=$info[2]===IMAGETYPE_PNG?@imagecreatefrompng($real):($info[2]===IMAGETYPE_GIF?@imagecreatefromgif($real):(function_exists('imagecreatefromwebp')?@imagecreatefromwebp($real):false)); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- an unreadable image keeps its own address. if(!$src)return ''; $im=imagecreatetruecolor($info[0],$info[1]);imagefill($im,0,0,imagecolorallocate($im,$rgb[0],$rgb[1],$rgb[2]));imagealphablending($im,true);imagecopy($im,$src,0,0,0,0,$info[0],$info[1]); ob_start();$made=imagepng($im,null,9);$png=(string)ob_get_clean();if(!$made)return ''; $tmp=$out.'.'.wp_generate_password(8,false).'.tmp'; // Written under a temporary name, then moved into place: a mail sent meanwhile never links a half-written file. $ok=wp_mkdir_p($dir)&&file_put_contents($tmp,$png)===strlen($png)&&@rename($tmp,$out); // phpcs:ignore WordPress.WP.AlternativeFunctions, WordPress.PHP.NoSilencedErrors.Discouraged -- a file in uploads; a failure keeps the store's logo. if(!$ok&&is_file($tmp))wp_delete_file($tmp); return $ok?$link:''; } /** * The colour behind the logo: the mail body in WooCommerce's newer e-mail design, the page background in the older one. * The StoreRuler theme paints those with its kit's colours (its e-mail stylesheet), so its palette answers first. */ private static function mail_background(): string { $improved=get_option('woocommerce_feature_email_improvements_enabled')==='yes'; $colour=strtolower((string)get_option($improved?'woocommerce_email_body_background_color':'woocommerce_email_background_color')); if(function_exists('storeruler_email_palette')&&!(function_exists('storeruler_email_block_editor')&&storeruler_email_block_editor())){$kit=(array)storeruler_email_palette();$colour=strtolower((string)($kit[$improved?'canvas':'bg']??$colour));} if(preg_match('/^#[0-9a-f]{3}$/',$colour))$colour='#'.$colour[1].$colour[1].$colour[2].$colour[2].$colour[3].$colour[3]; return preg_match('/^#[0-9a-f]{6}$/',$colour)?$colour:'#ffffff'; } private static function permission(): void { Store::require_cap('manage_options'); } private static function locked(callable $work) { return Store::lock('email-connection',static function()use($work){ foreach(array(self::PROFILE,self::TEST,self::RATE,self::OBSERVED)as$key)wp_cache_delete($key,'options');wp_cache_delete('notoptions','options'); return $work(); }); } private static function profile(): array { $p=get_option(self::PROFILE,array());return is_array($p)?$p:array(); } private static function revision(array $p): string { return $p?(string)($p['revision']??''):'0'; } private static function expected(array $data,array $p): void { if(!is_string($data['expected_revision']??null)||!hash_equals(self::revision($p),$data['expected_revision']))throw new \InvalidArgumentException(I18n::t('Refresh the email setup before saving changes.')); } private static function crypto_ready(): bool { return function_exists('openssl_encrypt')&&function_exists('openssl_decrypt')&&in_array('aes-256-gcm',openssl_get_cipher_methods(),true); } private static function aad(): string { return 'storeruler-email-v1:'.get_current_blog_id(); } private static function binding(array $p): string { return self::aad().'|'.hash('sha256',(string)wp_json_encode(array($p['provider']??'',$p['host']??'',$p['port']??0,$p['security']??'',$p['from_email']??'',$p['from_name']??''))); } private static function key(): string { return hash('sha256',wp_salt('auth').'|'.wp_salt('secure_auth').'|'.self::aad(),true); } private static function seal(array $auth,array $p): string { if(!self::crypto_ready())throw new \RuntimeException(I18n::t('Secure email credential storage is unavailable on this server.')); $iv=random_bytes(12);$tag='';$encrypted=openssl_encrypt((string)wp_json_encode($auth),'aes-256-gcm',self::key(),OPENSSL_RAW_DATA,$iv,$tag,self::binding($p)); if($encrypted===false||strlen($tag)!==16)throw new \RuntimeException(I18n::t('Secure email credential storage is unavailable on this server.')); return (string)wp_json_encode(array('v'=>2,'iv'=>bin2hex($iv),'tag'=>bin2hex($tag),'data'=>bin2hex($encrypted))); } /** Lowercase hex only: a changed letter case is a changed box, never the same bytes. */ private static function unhex($value) { return is_string($value)&&$value!==''&&strlen($value)%2===0&&preg_match('/^[0-9a-f]+$/D',$value)?hex2bin($value):false; } private static function open(array $p): array { $cipher=$p['credentials']??null;$box=is_string($cipher)?json_decode($cipher,true):null; if(!self::crypto_ready()||!is_array($box)||($box['v']??null)!==2)throw new \RuntimeException(I18n::t('Saved email credentials could not be opened. Connect the account again.')); $iv=self::unhex($box['iv']??'');$tag=self::unhex($box['tag']??'');$body=self::unhex($box['data']??''); if(!is_string($iv)||strlen($iv)!==12||!is_string($tag)||strlen($tag)!==16||!is_string($body)||strlen($body)>8192)throw new \RuntimeException(I18n::t('Saved email credentials could not be opened. Connect the account again.')); $plain=openssl_decrypt($body,'aes-256-gcm',self::key(),OPENSSL_RAW_DATA,$iv,$tag,self::binding($p));$auth=is_string($plain)?json_decode($plain,true):null; if(!is_array($auth)||!is_string($auth['username']??null)||$auth['username']===''||!is_string($auth['password']??null)||$auth['password']==='')throw new \RuntimeException(I18n::t('Saved email credentials could not be opened. Connect the account again.')); return $auth; } /** Hook counts are not conflicts: WooCommerce and other plugins legitimately format email here. */ private static function other_mailer(): string { $active=(array)get_option('active_plugins',array());if(is_multisite())$active=array_merge($active,array_keys((array)get_site_option('active_sitewide_plugins',array()))); foreach(array('wp-mail-smtp/'=>'wp_mail_smtp','fluent-smtp/'=>'fluent_smtp','post-smtp/'=>'post_smtp','easy-wp-smtp/'=>'easy_wp_smtp','smtp-mailer/'=>'smtp_mailer','wp-smtp/'=>'wp_smtp')as$prefix=>$id)foreach($active as$plugin)if(is_string($plugin)&&str_starts_with($plugin,$prefix))return $id; if(function_exists('wp_mail')){try{$file=(new \ReflectionFunction('wp_mail'))->getFileName();if($file&&wp_normalize_path($file)!==wp_normalize_path(ABSPATH.WPINC.'/pluggable.php'))return 'wordpress_handler';}catch(\ReflectionException $e){return 'wordpress_handler';}} $seen=get_option(self::OBSERVED,array());$p=self::profile();return is_array($seen)&&($seen['revision']??'')===self::revision($p)&&($seen['external']??false)?'wordpress_handler':''; } /** The email plugin that sends the store's email, by name, or '' (admin audit B7: the customer email card did not know WP Mail SMTP). */ public static function mailer_plugin(): string { return array('wp_mail_smtp'=>'WP Mail SMTP','fluent_smtp'=>'FluentSMTP','post_smtp'=>'Post SMTP','easy_wp_smtp'=>'Easy WP SMTP','smtp_mailer'=>'SMTP Mailer','wp_smtp'=>'WP SMTP')[self::other_mailer()]??''; } private static function remember_external(array $p): void { update_option(self::OBSERVED,array('revision'=>self::revision($p),'external'=>true,'at'=>time()),false); } public static function read(): array { self::permission();$p=self::profile();$test=get_option(self::TEST,array());$test=is_array($test)?$test:array();$other=self::other_mailer();$user=wp_get_current_user(); $current=($test['revision']??'')===self::revision($p)&&($test['by']??0)===get_current_user_id();$accepted=$current&&($test['status']??'')==='accepted';$confirmed=$accepted&&!empty($test['confirmed_at']); return array('provider'=>$p['provider']??'','host'=>$p['host']??'','port'=>$p['port']??465,'security'=>$p['security']??'ssl','from_email'=>$p['from_email']??'','from_name'=>$p['from_name']??'', 'revision'=>self::revision($p),'configured'=>!empty($p['credentials']),'enabled'=>!empty($p['enabled']),'has_credentials'=>!empty($p['credentials']),'crypto_ready'=>self::crypto_ready(),'other_mailer'=>$other, 'test_recipient'=>is_email($user->user_email)?$user->user_email:'','can_test'=>is_email($user->user_email)!==false, 'can_enable'=>!$other&&!empty($p['credentials'])&&self::crypto_ready()&&$confirmed&&($test['mode']??'')==='connection'&&!empty($test['own_init']), 'last_test'=>$current?array('id'=>$test['id']??'','mode'=>$test['mode']??'existing','status'=>$test['status']??'','at'=>!empty($test['at'])?gmdate('c',(int)$test['at']):'','inbox_confirmed'=>$confirmed,'can_confirm'=>$accepted&&!$confirmed&&($test['expires']??0)>=time()&&($test['attempts']??0)<5,'error_code'=>$test['error_code']??''):null); } public static function save(array $data): array { self::permission();return self::locked(static function()use($data){ $old=self::profile();self::expected($data,$old);$provider=Store::text($data,'provider'); if(!in_array($provider,array('hostinger','gmail','custom'),true))throw new \InvalidArgumentException(I18n::t('Choose a supported email provider.')); $security=Store::text($data,'security');$port=(int)($data['port']??0);$host=strtolower(Store::text($data,'host')); if($provider==='hostinger')$host='smtp.hostinger.com';if($provider==='gmail')$host='smtp.gmail.com'; if(($security!=='ssl'||$port!==465)&&($security!=='tls'||$port!==587))throw new \InvalidArgumentException(I18n::t('Use a public email server with secure port 465 or 587.')); self::hostname($host);$from=Store::text($data,'from_email');$name=Store::text($data,'from_name'); if(!is_email($from)||strlen($from)>254||$name===''||mb_strlen($name)>120||preg_match('/[\x00-\x1f\x7f]/',$from.$name))throw new \InvalidArgumentException(I18n::t('Enter a valid email account and sender name.')); $username=$data['username']??'';$password=$data['password']??''; if(!is_string($username)||!is_string($password)||strlen($username)>320||strlen($password)>1024||preg_match('/[\x00-\x1f\x7f]/',$username.$password))throw new \InvalidArgumentException(I18n::t('Enter the email account username and password.')); $same=($old['provider']??'')===$provider&&($old['host']??'')===$host&&($old['port']??0)===$port&&($old['security']??'')===$security&&($old['from_email']??'')===$from; $auth=$same&&!empty($old['credentials'])&&($username===''||$password==='')?self::open($old):array(); if($password===''&&$username!==''&&isset($auth['username'])&&$username!==$auth['username'])throw new \InvalidArgumentException(I18n::t('Enter the email account username and password.')); if($username==='')$username=$auth['username']??'';if($password==='')$password=$auth['password']??''; if($provider==='gmail')$password=str_replace(' ','',$password); if($provider==='gmail'&&!preg_match('/^[a-zA-Z0-9]{16}$/D',$password))throw new \InvalidArgumentException(I18n::t('Use a 16-character Gmail app password.')); if($username===''||$password===''||($provider!=='custom'&&strcasecmp($username,$from)!==0))throw new \InvalidArgumentException(I18n::t('Enter the email account username and password.')); $p=array('v'=>1,'provider'=>$provider,'host'=>$host,'port'=>$port,'security'=>$security,'from_email'=>$from,'from_name'=>$name,'revision'=>wp_generate_uuid4(),'enabled'=>false);$p['credentials']=self::seal(array('username'=>$username,'password'=>$password),$p); update_option(self::PROFILE,$p,false);delete_option(self::TEST);delete_option(self::OBSERVED);return self::read(); }); } private static function hostname(string $host): void { if(strlen($host)>253||filter_var($host,FILTER_VALIDATE_IP)||!preg_match('/^(?=.{3,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/D',$host))throw new \InvalidArgumentException(I18n::t('Use a public email server with secure port 465 or 587.')); } private static function public_ip(string $ip): bool { if(!filter_var($ip,FILTER_VALIDATE_IP,FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE))return false; $packed=inet_pton($ip);if($packed===false)return false; if(strlen($packed)===16)return (ord($packed[0])&0xe0)===0x20; // Globally routed IPv6 only, excluding mapped/local/multicast addresses. $n=unpack('N',$packed)[1]; foreach(array(array('0.0.0.0',8),array('10.0.0.0',8),array('100.64.0.0',10),array('127.0.0.0',8),array('169.254.0.0',16),array('172.16.0.0',12),array('192.0.0.0',24),array('192.0.2.0',24),array('192.168.0.0',16),array('198.18.0.0',15),array('198.51.100.0',24),array('203.0.113.0',24),array('224.0.0.0',4),array('240.0.0.0',4))as$range){$mask=(0xffffffff<<(32-$range[1]))&0xffffffff;if(($n&$mask)===(unpack('N',inet_pton($range[0]))[1]&$mask))return false;} return true; } /** Resolve once, validate every answer, then connect to one pinned IP with the original hostname's TLS identity. */ private static function address(string $host): string { self::hostname($host);$records=apply_filters('storesuit_email_dns_addresses',null,$host); if($records===null){$records=array();foreach((array)dns_get_record($host,DNS_A|DNS_AAAA)as$r){if(isset($r['ip']))$records[]=$r['ip'];if(isset($r['ipv6']))$records[]=$r['ipv6'];}} if(!is_array($records)||!$records||count($records)>64)throw new \RuntimeException(I18n::t('The email connection could not be used.')); foreach($records as$ip)if(!is_string($ip)||!self::public_ip($ip))throw new \RuntimeException(I18n::t('Use a public email server with secure port 465 or 587.')); $ip=$records[0];return str_contains($ip,':')?'['.$ip.']':$ip; } public static function observe_pre($pre,array $atts) { // A later-registered callback at the same priority must get its chance before we examine the actual transport. remove_action('phpmailer_init',array(__CLASS__,'mailer'),PHP_INT_MAX);add_action('phpmailer_init',array(__CLASS__,'mailer'),PHP_INT_MAX); self::$sender=array(); if($pre!==null){if(self::$probe&&self::$probe['mode']==='connection')self::$probe['external']=true;$p=self::profile();if(!empty($p['enabled']))self::remember_external($p);} return $pre; } private static function sender_profile(): ?array { $p=self::$probe?self::$probe['profile']:self::profile();$using=self::$probe&&self::$probe['mode']==='connection'; return ($using||!empty($p['enabled']))&&!self::other_mailer()&&is_email($p['from_email']??'')&&is_string($p['from_name']??null)?$p:null; } public static function from_address($address) { $p=self::sender_profile();if(!$p)return $address;self::$sender['address']=$address;return $p['from_email']; } public static function from_name($name) { $p=self::sender_profile();if(!$p)return $name;self::$sender['name']=$name;return $p['from_name']; } private static function capture($mail): void { $values=array();foreach(array('Mailer','Host','Port','SMTPSecure','SMTPAutoTLS','SMTPAuth','Username','Password','SMTPOptions','SMTPDebug','Debugoutput','SMTPKeepAlive','Timeout','Timelimit','AuthType','From','FromName','Sender')as$key)$values[$key]=$mail->$key; self::$restore=array('mailer'=>$mail,'values'=>$values,'smtp'=>$mail->getSMTPInstance()); } private static function restore_mailer(): void { $saved=self::$restore;self::$restore=null;self::$transport='';if(!$saved)return;$mail=$saved['mailer']; try{$mail->smtpClose();}catch(\Throwable $e){unset($e);}finally{foreach($saved['values']as$key=>$value)$mail->$key=$value;$mail->setSMTPInstance($saved['smtp']);} } private static function transport_identity($mail): string { return hash_hmac('sha256',(string)wp_json_encode(array($mail->Mailer,$mail->Host,$mail->Port,$mail->SMTPSecure,$mail->SMTPAutoTLS,$mail->SMTPAuth,$mail->Username,$mail->Password,$mail->SMTPOptions,$mail->SMTPDebug,$mail->From,$mail->FromName,$mail->Sender,$mail->AuthType,spl_object_id($mail->getSMTPInstance()))),self::key()); } private static function refused($mail): void { $mail->isSMTP();$mail->Host='storeruler-email-refused.invalid';$mail->SMTPAuth=false;$mail->Username='';$mail->Password='';$mail->SMTPDebug=0;$mail->Debugoutput=static function(){};$mail->SMTPKeepAlive=false; $mail->setSMTPInstance(new class extends \PHPMailer\PHPMailer\SMTP { public function connect($host,$port=null,$timeout=30,$options=array()){$this->setError('The email connection could not be used.');return false;} }); } public static function mailer($mail): void { $probe=self::$probe; if($probe){$to=$mail->getToAddresses();if(count($to)!==1||strcasecmp($to[0][0],$probe['recipient'])!==0||$mail->Subject!==$probe['subject']||$mail->getCcAddresses()||$mail->getBccAddresses())return;$p=$probe['profile'];if($probe['mode']!=='connection'&&empty($p['enabled']))return;} else{$p=self::profile();if(empty($p['enabled']))return;} if(self::other_mailer()||$mail->Mailer!=='mail'||$mail->getSMTPInstance()->connected()){ // An unknown handler may set its transport here. Undo only our early sender replacement if its original was valid. if($mail->From===($p['from_email']??'')&&is_string(self::$sender['address']??null)&&is_email(self::$sender['address']))$mail->setFrom(self::$sender['address'],is_string(self::$sender['name']??null)?self::$sender['name']:$mail->FromName,false); if(self::$probe)self::$probe['external']=true;else self::remember_external($p); return; // Existing transport keeps complete ownership; never overwrite it. } self::$own_attempt=true; self::capture($mail); try{ $auth=self::open($p);unset($auth);$ip=self::address($p['host']); $mail->isSMTP();$mail->Host=$ip;$mail->Port=$p['port'];$mail->SMTPSecure=$p['security'];$mail->SMTPAutoTLS=false;$mail->SMTPAuth=true;$mail->AuthType=''; // Keep secrets out of the shared PHPMailer object. Only our bound SMTP instance opens them for native authentication. $mail->Username='';$mail->Password='';$mail->SMTPDebug=0;$mail->Debugoutput=static function(){};$mail->SMTPKeepAlive=false;$mail->Timeout=15;$mail->Timelimit=20; $crypto=STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;if(defined('STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT'))$crypto|=STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT; $mail->SMTPOptions=array('ssl'=>array('verify_peer'=>true,'verify_peer_name'=>true,'allow_self_signed'=>false,'peer_name'=>$p['host'],'SNI_enabled'=>true,'crypto_method'=>$crypto)); // Only the connected account becomes the sender; message content, recipients, replies, headers and attachments stay intact. $mail->setFrom($p['from_email'],$p['from_name'],false);$mail->Sender=$p['from_email']; // Native SMTP remains the sender. This thin guard refuses changed transport identity before any socket/authentication. $guard=static fn()=>self::$transport!==''&&hash_equals(self::$transport,self::transport_identity($mail)); $auth=static fn()=>self::open($p); $mail->setSMTPInstance(new class($guard,$auth,$p['security']) extends \PHPMailer\PHPMailer\SMTP { private $guard;private $auth;private string $mode;private bool $secure=false; public function __construct(callable $guard,callable $auth,string $mode){$this->guard=$guard;$this->auth=$auth;$this->mode=$mode;} public function connect($host,$port=null,$timeout=30,$options=array()){if(!call_user_func($this->guard)){$this->setError('The email connection could not be used.');return false;}$connected=parent::connect($host,$port,$timeout,$options);$this->secure=$connected&&$this->mode==='ssl';return $connected;} public function startTLS(){if(!call_user_func($this->guard))return false;$this->secure=parent::startTLS();return $this->secure;} public function authenticate($username,$password,$authtype=null,$OAuth=null){if(!$this->secure||!call_user_func($this->guard)){$this->setError('The email connection could not be used.');return false;}try{$auth=call_user_func($this->auth);return parent::authenticate($auth['username'],$auth['password'],$authtype,$OAuth);}catch(\Throwable $e){$this->setError('The email connection could not be used.');return false;}finally{unset($auth);}} }); self::$transport=self::transport_identity($mail);if($probe)self::$probe['own_init']=true; }catch(\Throwable $e){self::refused($mail);} // phpmailer_init runs outside wp_mail's try; refuse inside its normal send/catch path. } public static function failed($error): void { if(!self::$own_attempt)return; if($error instanceof \WP_Error){$data=$error->get_error_data();foreach($error->get_error_codes()as$code)$error->remove($code);$error->add('wp_mail_failed',I18n::t('The email connection could not be used.'),$data);} self::$own_attempt=false; self::restore_mailer(); } public static function succeeded(array $data): void { if(self::$own_attempt&&self::$restore&&self::$transport!==''&&!hash_equals(self::$transport,self::transport_identity(self::$restore['mailer']))){if(self::$probe)self::$probe['external']=true;else self::remember_external(self::profile());} self::$own_attempt=false;self::restore_mailer(); } public static function test(array $data): array { self::permission();if(empty($data['confirmed']))throw new \InvalidArgumentException(I18n::t('Send one test email to your WordPress account to continue.')); return self::locked(static function()use($data){ $p=self::profile();self::expected($data,$p);$mode=Store::text($data,'mode');if(!in_array($mode,array('existing','connection'),true)||($mode==='connection'&&empty($p['credentials'])))throw new \InvalidArgumentException(I18n::t('Send one test email to your WordPress account to continue.')); if($mode==='connection'&&self::other_mailer())throw new \InvalidArgumentException(I18n::t('Another email connection is already handling WordPress email. Use its settings.')); $recipient=wp_get_current_user()->user_email;if(!is_email($recipient))throw new \InvalidArgumentException(I18n::t('Enter a valid email account and sender name.')); $rate=get_option(self::RATE,array());$rate=is_array($rate)?$rate:array();$now=time();$day=gmdate('Y-m-d');$count=($rate['day']??'')===$day?(int)($rate['count']??0):0; if($now-(int)($rate['at']??0)<60||$count>=10)throw new \RuntimeException(I18n::t('Wait before sending another test email.')); update_option(self::RATE,array('at'=>$now,'day'=>$day,'count'=>$count+1),false); $id=wp_generate_uuid4();$code=(string)random_int(100000,999999);$subject=I18n::t('StoreRuler email setup test');$body=sprintf(I18n::t('Your StoreRuler email setup code is: %s'),$code)."\n\n".I18n::t('Enter this code in Settings to confirm that the test email reached your inbox.'); $record=array('id'=>$id,'revision'=>self::revision($p),'by'=>get_current_user_id(),'mode'=>$mode,'at'=>$now,'expires'=>$now+600,'attempts'=>0,'status'=>'failed','own_init'=>false,'confirmed_at'=>0,'code_hash'=>hash_hmac('sha256',$code.'|'.$id.'|'.get_current_user_id().'|'.self::revision($p),wp_salt('auth')),'error_code'=>'transport_failed'); self::$probe=array('mode'=>$mode,'profile'=>$p,'recipient'=>$recipient,'subject'=>$subject,'own_init'=>false,'external'=>false); try{$accepted=wp_mail($recipient,$subject,$body,array('Content-Type: text/plain; charset=UTF-8'));$record['own_init']=self::$probe['own_init'];$external=self::$probe['external'];$record['status']=$accepted&&($mode==='existing'||($record['own_init']&&!$external))?'accepted':'failed';$record['error_code']=$record['status']==='accepted'?'':($external?'external_mailer':'transport_failed');} catch(\Throwable $e){$record['error_code']='transport_failed';} finally{try{self::restore_mailer();}finally{self::$probe=null;self::$own_attempt=false;}} update_option(self::TEST,$record,false);return self::read(); }); } public static function confirm(array $data): array { self::permission();return self::locked(static function()use($data){ $p=self::profile();$test=get_option(self::TEST,array());$id=Store::text($data,'test_id');$code=$data['code']??''; if(!is_array($test)||($test['id']??'')!==$id||($test['by']??0)!==get_current_user_id()||($test['revision']??'')!==self::revision($p)||($test['status']??'')!=='accepted')throw new \InvalidArgumentException(I18n::t('Enter the code from the latest test email.')); if(($test['expires']??0)=5)throw new \InvalidArgumentException(I18n::t('Too many incorrect codes. Send a new test email.')); $test['attempts']++;$valid=is_string($code)&&preg_match('/^\d{6}$/D',$code)&&hash_equals($test['code_hash'],hash_hmac('sha256',$code.'|'.$id.'|'.get_current_user_id().'|'.self::revision($p),wp_salt('auth'))); if($valid)$test['confirmed_at']=time();update_option(self::TEST,$test,false); if(!$valid)throw new \InvalidArgumentException(I18n::t('Enter the code from the latest test email.'));return self::read(); }); } public static function enable(array $data): array { self::permission();return self::locked(static function()use($data){$p=self::profile();self::expected($data,$p);if(empty($data['confirmed'])||!self::read()['can_enable'])throw new \InvalidArgumentException(I18n::t('Enter the code from the latest test email.'));$p['enabled']=true;update_option(self::PROFILE,$p,false);return self::read();}); } public static function off(array $data): array { self::permission();return self::locked(static function()use($data){$p=self::profile();self::expected($data,$p);if($p){$p['enabled']=false;update_option(self::PROFILE,$p,false);}return self::read();}); } public static function forget(array $data): array { self::permission();return self::locked(static function()use($data){$p=self::profile();self::expected($data,$p);if(empty($data['confirmed']))throw new \InvalidArgumentException(I18n::t('Confirm disconnecting the saved email account.'));delete_option(self::PROFILE);delete_option(self::TEST);delete_option(self::OBSERVED);return self::read();}); } /** Freemius's uninstall callback only removes this private connector; business records remain. */ public static function uninstall(): void { foreach(array(self::PROFILE,self::TEST,self::RATE,self::OBSERVED)as$key)delete_option($key); } }