last_error=''; $user=get_user_by('email',$normal); $customerId=$user instanceof WP_User?(int)$user->ID:0; if($wpdb->last_error!==''){return false;} $customers=array($normal);if($customerId>0){$customers[]=$customerId;} $statuses=array_values(array_unique(array_merge(array_keys(wc_get_order_statuses()),array('trash','checkout-draft')))); $orders=wc_get_orders(array('type'=>'shop_order','customer'=>$customers,'status'=>$statuses,'limit'=>2501,'orderby'=>'ID','order'=>'ASC','return'=>'objects','paginate'=>false)); if($wpdb->last_error!=='' || !is_array($orders) || !array_is_list($orders) || count($orders)>2500){return false;} foreach($orders as $order){ if(!$order instanceof WC_Order || $order->get_type()!=='shop_order'){return false;} $billingEmail=strtolower(trim((string)$order->get_billing_email('edit'))); if($billingEmail!==$normal && !($customerId>0 && (int)$order->get_customer_id('edit')===$customerId)){return false;} $orderHashes=orderguard_gdpr_erasure_order_flag_hashes($order); if(!is_array($orderHashes)){return false;} $hashes=array_merge($hashes,$orderHashes); } $hashes=array_values(array_unique(array_filter($hashes,'is_string')));sort($hashes,SORT_STRING); $ok=false;$reason=''; $rows=orderguard_gdpr_local_flag_rows_for_hashes($hashes,$ok,$reason); return $ok && is_array($rows) && !$rows; } finally {orderguard_release_gdpr_erasure_lifecycle_lock();} } public function canAttach(): bool { $hook='wp_privacy_personal_data_erasure_page';$legacy='orderguard_filter_personal_data_erasure_page';$old=array('OG_Owned_Native_Coordinator','filter'); foreach(($GLOBALS['wp_filter'][$hook]->callbacks??array()) as $priority=>$entries){foreach($entries as $entry){ $callback=$entry['function']; if(($callback===$legacy||$callback===$old)&&$priority!==9){return false;} if(is_array($callback)&&($callback[0]??null) instanceof self&&$callback[0]!==$this){return false;} }} return true; } public function attach(): bool { if(!$this->canAttach()){return false;} $hook='wp_privacy_personal_data_erasure_page'; remove_filter($hook,'orderguard_filter_personal_data_erasure_page',9); remove_filter($hook,array('OG_Owned_Native_Coordinator','filter'),9); add_filter($hook,array($this,'filter'),9,6); return true; } public function filter($response,$index,$email,$page,$id,$key) { if($key!=='orderguard' || is_wp_error($response)){return $response;} // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- applies an existing WordPress or WooCommerce filter; not a new hook $erasers=apply_filters('wp_privacy_personal_data_erasers',array()); if(!OG_OrderGuard_Package_Bootstrap::localOnlyEligible() || !is_int($index)||$index!==1 || !is_int($page)||$page<1 || !is_int($id)||$id<1 || !is_string($email) || !is_array($erasers)||array_key_first($erasers)!=='orderguard'||($erasers['orderguard']['callback']??null)!=='orderguard_personal_data_eraser_placeholder'){ return new WP_Error('orderguard_local_erasure_unavailable',__('OrderGuard cannot safely complete its local erasure step.','orderguard')); } if(!$this->localIdentityIsClear($email)){ return new WP_Error('orderguard_local_data_pending',__('OrderGuard found local customer records or could not prove that its local records are clear. Connect and configure OrderGuard before retrying this erasure.','orderguard')); } return array('items_removed'=>false,'items_retained'=>false,'messages'=>array(__('OrderGuard has never connected on this store, so it has no network data to erase.','orderguard')),'done'=>true); } } /** Package composition root, loaded before the legacy activation callback. */ final class OG_OrderGuard_Package_Bootstrap { const EVER_CONNECTED_OPTION='orderguard_owned_ever_connected'; private static string $attached=''; private static $eraser=null; private static function registries(): array { $bundled=require __DIR__.'/receipt-key-registry.php'; if(!is_array($bundled)){throw new UnexpectedValueException('registry_invalid');} $registry=$bundled; $dev=function_exists('orderguard_dev_mode')&&orderguard_dev_mode(); foreach(array('active'=>'ORDERGUARD_NATIVE_RECEIPT_KEYS','history'=>'ORDERGUARD_NATIVE_RECEIPT_KEY_HISTORY','revoked'=>'ORDERGUARD_NATIVE_RECEIPT_REVOKED') as $field=>$constant){ if(!is_array($bundled[$field]??null)){throw new UnexpectedValueException('registry_invalid');} if(defined($constant)){ if(!is_array(constant($constant))){throw new UnexpectedValueException('registry_invalid');} // Outside explicit development mode, verification keys (active and // history) come only from the reviewed bundled registry: a // configuration constant may neither replace nor add them. Only // revocations may be added, and they never remove a bundled one. if($field!=='revoked'&&!$dev){throw new UnexpectedValueException('registry_override_refused');} $registry[$field]=constant($constant); } } // Validate both sides before the union: malformed or false flags must // not be hidden by a same-named bundled entry. OG_Native_Receipt_Keys::active(array(),$bundled['revoked']); OG_Native_Receipt_Keys::active(array(),$registry['revoked']); $registry['revoked']=$bundled['revoked']+$registry['revoked']; $registry['environment']=defined('ORDERGUARD_NATIVE_ENVIRONMENT')?constant('ORDERGUARD_NATIVE_ENVIRONMENT'):($bundled['environment']??null); if(!is_string($registry['environment']) || !in_array($registry['environment'],array('production','review'),true) || (!$dev && $registry['environment']!=='production')){throw new UnexpectedValueException('registry_environment');} foreach(array('active','history') as $field){ OG_Native_Remote_Binding::validateRegistry($bundled[$field]); $registry[$field]=OG_Native_Receipt_Keys::active($registry[$field],$registry['revoked']); foreach($registry[$field] as $id=>$public){ $prefix=$registry['environment']==='production'?'prod-':'test-'; if(!str_starts_with($id,$prefix)){throw new UnexpectedValueException('registry_environment');} } } foreach($registry['active'] as $id=>$public){ if(isset($registry['history'][$id]) && $registry['history'][$id]!==$public){throw new UnexpectedValueException('registry_conflict');} } return $registry; } private static function apiKey(): string { $key=function_exists('orderguard_get_api_key')?orderguard_get_api_key():''; return is_string($key)?trim($key):''; } private static function rememberConnection(string $apiKey): void { if($apiKey!=='' && !get_option(self::EVER_CONNECTED_OPTION,false)){update_option(self::EVER_CONNECTED_OPTION,1,false);} } /** Record the irreversible marker in the same request that stores a credential. */ public static function apiKeySaved($oldValue,$newValue): void { self::rememberConnection(is_string($newValue)?trim($newValue):''); } public static function activate($networkWide=false): void {OG_Owned_Install_Controller::activate($networkWide===true);} public static function localOnlyEligible(): bool { $snapshot=OG_Native_Legacy_Gate::snapshot(); return ($snapshot['state']??null)==='active' && ($snapshot['mode']??null)==='fresh' && self::apiKey()==='' && !get_option(self::EVER_CONNECTED_OPTION,false); } /** Non-mutating proof used before an existing-store lifecycle changes. */ public static function nativeReadiness(bool $requireRemote=false,bool $checkCallback=true): array { $apiKey=self::apiKey(); if($apiKey===''){return array('ready'=>false,'reason'=>'erasure_api_credential_unavailable');} try{$registry=self::registries();}catch(Throwable $e){return array('ready'=>false,'reason'=>'erasure_receipt_key_unavailable');} $keys=$registry['active'];$history=$registry['history'];$revoked=$registry['revoked'];$environment=$registry['environment']; if(!is_array($keys)||!$keys){return array('ready'=>false,'reason'=>'erasure_receipt_key_unavailable');} try { OG_Native_Receipt_Keys::active($keys,$revoked); $admission=static function()use($apiKey,$registry,$keys,$revoked,$environment): bool { // A changed credential or registry requires a new composition // on the next request; never prove one identity and send another. return $environment==='production' && self::apiKey()===$apiKey && self::registries()===$registry && OG_Native_Readiness_Proof::remote($apiKey,$keys,$revoked); }; $eraser=new OG_Native_Recovery_Eraser(new OG_Native_Scheduled_HTTP($apiKey,$admission),$keys,$environment,$history,$revoked,true,$admission); if($checkCallback&&!$eraser->canAttach()){return array('ready'=>false,'reason'=>'erasure_callback_inventory_unsafe');} if($requireRemote && ($environment!=='production'||!OG_Native_Readiness_Proof::remote($apiKey,$keys,$revoked))){return array('ready'=>false,'reason'=>'erasure_server_proof_unavailable');} return array('ready'=>true,'reason'=>'erasure_engine_ready','eraser'=>$eraser); }catch(Throwable $e){return array('ready'=>false,'reason'=>'erasure_receipt_key_unavailable');} } public static function attachNativeEraser(): bool { if(self::$attached==='native'){return true;} if(OG_Native_Legacy_Gate::state()!=='active'){return false;} $ready=self::nativeReadiness();if(!$ready['ready']){return false;} $eraser=$ready['eraser']; if(self::$attached==='local' && self::$eraser instanceof OG_OrderGuard_Local_Only_Eraser){remove_filter('wp_privacy_personal_data_erasure_page',array(self::$eraser,'filter'),9);} if(!$eraser->attach()){return false;} self::$eraser=$eraser;self::$attached='native';return true; } public static function attachLocalOnlyEraser(): bool { if(self::$attached==='local'){return true;} if(!self::localOnlyEligible()){return false;} $eraser=new OG_OrderGuard_Local_Only_Eraser(); if(!$eraser->attach()){return false;} self::$eraser=$eraser;self::$attached='local';return true; } public static function runtime(): void { $apiKey=self::apiKey();self::rememberConnection($apiKey); if(self::localOnlyEligible()){self::attachLocalOnlyEraser();return;} if(self::attachNativeEraser()){ if(!wp_next_scheduled('orderguard_native_receipt_ack_tick')){wp_schedule_event(time()+300,'hourly','orderguard_native_receipt_ack_tick');} if(!wp_next_scheduled('orderguard_native_restart_tick')){wp_schedule_event(time()+600,'hourly','orderguard_native_restart_tick');} } } /** Trusted WP completion hook: schedules only an ID, never credentials or * receipt bytes. Periodic discovery recovers a lost scheduling event. */ public static function receiptSaved($requestId): void { if(self::$attached!=='native'||!is_int($requestId)||$requestId<1){return;} if(!wp_next_scheduled('orderguard_native_receipt_ack_one',array($requestId))){ wp_schedule_single_event(time()+5,'orderguard_native_receipt_ack_one',array($requestId)); } } public static function receiptAckTick($requestId=null): void { // Re-evaluate the store state and current credential/registry locally; // ACKs do not need a remote capability proof or authorize any erasure. if(OG_Native_Legacy_Gate::state()!=='active'||!self::nativeReadiness(false,false)['ready']){return;} if($requestId!==null&&(!is_int($requestId)||$requestId<1)){return;} try { global $wpdb;$context=(new OG_Owned_WordPress_Context($wpdb))->context(); $registry=self::registries();$transport=new \OrderGuard\NativeHTTP\Transport(self::apiKey()); if($requestId!==null){$ids=array($requestId);$page=null;} else{ $cursor=get_option('orderguard_native_ack_cursor',array()); $after=is_array($cursor)&&($cursor['generation']??null)===$context['generation']&&is_int($cursor['after']??null)&&$cursor['after']>=0?$cursor['after']:0; $page=$context['writer']->nativeReceiptAckCandidates($context,$after); if(($page['status']??null)!=='native_ack_candidates'){return;}$ids=$page['request_ids']; } foreach($ids as $id){ OG_Native_Ack_Sender::once($context['writer'],$context,$id,$transport,$registry['active'],$registry['environment'],$registry['history'],$registry['revoked']); } if($page!==null){update_option('orderguard_native_ack_cursor',array('generation'=>$context['generation'],'after'=>$page['next_cursor']),false);} }catch(Throwable $e){/* Receipt remains durable; do not print credentials or mark erasure done. */} } /** Restart queue transport, or null while the store is not active or readiness refuses. */ public static function restartClient(): ?OG_Native_Restart_Client { if(OG_Native_Legacy_Gate::state()!=='active'||!self::nativeReadiness(false,false)['ready']){return null;} return new \OrderGuard\NativeHTTP\Transport(self::apiKey()); } /** Hourly bounded poll of open restart requests. Fetching the owner's * decision is all it does; sending the new operation stays with the * ordinary erasure workflow, and no completion is inferred. */ public static function restartTick(): void { $client=self::restartClient();if($client===null){return;} try { global $wpdb;$context=(new OG_Owned_WordPress_Context($wpdb))->context();$registry=self::registries(); $cursor=get_option('orderguard_native_restart_cursor',array()); $after=is_array($cursor)&&($cursor['generation']??null)===$context['generation']&&is_int($cursor['after']??null)&&$cursor['after']>=0?$cursor['after']:0; $page=$context['writer']->nativeRestartCandidates($context,$after); if(($page['status']??null)!=='native_restart_candidates'){return;} foreach($page['request_ids'] as $id){OG_Native_Restart_Sender::once($context['writer'],$context,$id,$client,$registry['active'],$registry['environment'],$registry['history'],$registry['revoked']);} update_option('orderguard_native_restart_cursor',array('generation'=>$context['generation'],'after'=>$page['next_cursor']),false); }catch(Throwable $e){/* The draft stays durable; nothing is erased or completed. */} } public static function registerAttention(): void { try{$registry=self::registries();$unavailable=false;} catch(Throwable $e){ // A refused or unreadable key registry must stay visible: the page and its // warnings remain, but with no usable keys and no restart client every // action stays refused (configurationNotice() explains what to fix). $registry=array('active'=>array(),'environment'=>'production','history'=>array(),'revoked'=>array());$unavailable=true; } OG_Native_Recovery_Admin::register($registry['active'],$registry['environment'],$registry['history'],$registry['revoked'], $unavailable?static function(){return null;}:static function(){return self::restartClient();},$unavailable); } /** wp-config constants that make OrderGuard refuse its verification keys. * Returns constant NAMES only; values (keys) are never read out or shown. */ public static function configurationProblems(): array { $dev=function_exists('orderguard_dev_mode')&&orderguard_dev_mode();$bad=array(); foreach(array('ORDERGUARD_NATIVE_RECEIPT_KEYS','ORDERGUARD_NATIVE_RECEIPT_KEY_HISTORY') as $constant){ if(!defined($constant)){continue;} $value=constant($constant); if(!$dev||!is_array($value)){$bad[]=$constant;continue;} try{OG_Native_Receipt_Keys::active($value,array());}catch(Throwable $e){$bad[]=$constant;} } if(defined('ORDERGUARD_NATIVE_RECEIPT_REVOKED')){ $value=constant('ORDERGUARD_NATIVE_RECEIPT_REVOKED'); try{if(!is_array($value)){throw new UnexpectedValueException('registry_invalid');}OG_Native_Receipt_Keys::active(array(),$value);} catch(Throwable $e){$bad[]='ORDERGUARD_NATIVE_RECEIPT_REVOKED';} } if(defined('ORDERGUARD_NATIVE_ENVIRONMENT')){ $value=constant('ORDERGUARD_NATIVE_ENVIRONMENT'); if(!is_string($value)||!in_array($value,array('production','review'),true)||(!$dev&&$value!=='production')){$bad[]='ORDERGUARD_NATIVE_ENVIRONMENT';} } if(defined('ORDERGUARD_EXCLUSION_RECEIPT_KEYS')&&(!$dev||!is_array(constant('ORDERGUARD_EXCLUSION_RECEIPT_KEYS')))){$bad[]='ORDERGUARD_EXCLUSION_RECEIPT_KEYS';} if(defined('ORDERGUARD_EXCLUSION_RECEIPT_ENVIRONMENT')){ $value=constant('ORDERGUARD_EXCLUSION_RECEIPT_ENVIRONMENT'); if(!is_string($value)||(!$dev&&$value!=='production')){$bad[]='ORDERGUARD_EXCLUSION_RECEIPT_ENVIRONMENT';} } return $bad; } /** One notice for every store state (prepared, active, legacy), naming only the * affected service: the native erasure keys and the exclusion-removal keys are * separate registries, so each group is reported and advised on its own. */ public static function configurationNotice(): void { if(!current_user_can('manage_options')){return;} $bad=self::configurationProblems(); if($bad){ $native=array_values(array_intersect($bad,array('ORDERGUARD_NATIVE_RECEIPT_KEYS','ORDERGUARD_NATIVE_RECEIPT_KEY_HISTORY','ORDERGUARD_NATIVE_RECEIPT_REVOKED','ORDERGUARD_NATIVE_ENVIRONMENT'))); $exclusion=array_values(array_intersect($bad,array('ORDERGUARD_EXCLUSION_RECEIPT_KEYS','ORDERGUARD_EXCLUSION_RECEIPT_ENVIRONMENT'))); $paragraphs=array(); if($native){ /* translators: %s: comma-separated wp-config.php constant names */ $paragraphs[]=sprintf(__('OrderGuard’s new erasure service is off on this site because wp-config.php defines %s. Nothing is sent or marked complete because of it.','orderguard'),implode(', ',$native)); $devOnly=array_values(array_intersect($native,array('ORDERGUARD_NATIVE_RECEIPT_KEYS','ORDERGUARD_NATIVE_RECEIPT_KEY_HISTORY'))); /* translators: %s: comma-separated wp-config.php constant names */ if($devOnly){$paragraphs[]=sprintf(__('%s is allowed only in development mode. Remove it from wp-config.php.','orderguard'),implode(', ',$devOnly));} if(in_array('ORDERGUARD_NATIVE_RECEIPT_REVOKED',$native,true)){$paragraphs[]=__('ORDERGUARD_NATIVE_RECEIPT_REVOKED is not a valid revocation list. Correct it so that it still lists every key ID you meant to revoke, each set to true. Do not simply delete it: a key you revoked could then be trusted again.','orderguard');} if(in_array('ORDERGUARD_NATIVE_ENVIRONMENT',$native,true)){$paragraphs[]=__('Outside development mode ORDERGUARD_NATIVE_ENVIRONMENT must be production. Change it to production or remove it.','orderguard');} } if($exclusion){ /* translators: %s: comma-separated wp-config.php constant names */ $paragraphs[]=sprintf(__('Removals from the OrderGuard network are waiting on this site because wp-config.php defines %s. Nothing is sent because of it.','orderguard'),implode(', ',$exclusion)); /* translators: %s: comma-separated wp-config.php constant names */ if(in_array('ORDERGUARD_EXCLUSION_RECEIPT_KEYS',$exclusion,true)){$paragraphs[]=sprintf(__('%s is allowed only in development mode. Remove it from wp-config.php.','orderguard'),'ORDERGUARD_EXCLUSION_RECEIPT_KEYS');} if(in_array('ORDERGUARD_EXCLUSION_RECEIPT_ENVIRONMENT',$exclusion,true)){$paragraphs[]=__('Outside development mode ORDERGUARD_EXCLUSION_RECEIPT_ENVIRONMENT must be production. Change it to production or remove it.','orderguard');} } echo '
'.esc_html($paragraph).'
';} echo ''.esc_html__('OrderGuard’s built-in erasure verification keys could not be read. Reinstall the plugin. Until then, the new erasure service stays off; nothing is sent or marked complete.','orderguard').'
'.esc_html__('This page lists erasure warnings and lets you ask OrderGuard’s server for proof that an erasure happened. A proof is only a record: it never restarts an erasure, cleans up data or marks a request complete. Warnings stay visible after a proof is checked again.','orderguard').'
'; echo ''.esc_html__('Checking for a proof does not restart a stopped erasure. Erasures that need attention or have run out of attempts stay stopped until they are handled on the OrderGuard erasure attention page.','orderguard').'
'; echo ''.esc_html__('If a warning says this site’s address did not match the address registered with OrderGuard, confirm the correct address with OrderGuard; if the site really moved, the registration may need updating. With an unchanged address you can check again 24 hours after the last mismatch; earlier checks send nothing. The warning stays visible after it is fixed.','orderguard').'
'; try{ global $wpdb;$context=(new OG_Owned_WordPress_Context($wpdb,false))->attentionContext(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- display paging value accepted only as 1-18 digits $after=isset($_GET['after'])&&is_string($_GET['after'])&&preg_match('/^[0-9]{1,18}$/D',$_GET['after'])?(int)$_GET['after']:0; $page=$context['writer']->listNativeAttention($context,$after); if(($page['status']??null)!=='native_attention_page'){throw new RuntimeException('unavailable');} foreach($page['entries'] as $entry){ $id=$entry['request_id']; /* translators: %d: WordPress personal data erasure request ID. */ $heading=sprintf(__('Request %d','orderguard'),$id); echo ''.esc_html__('No warnings to show here. This does not mean every erasure is complete.','orderguard').'
';} if($page['has_more']){echo ''.esc_html__('Next page','orderguard').'';} }catch(Throwable $e){echo ''.esc_html__('The saved warnings could not be read. Do not assume there are none.','orderguard').'
';} echo ''.esc_html__('OrderGuard cannot save erasure proofs because the WordPress security keys in wp-config.php are missing or invalid. Ask your hosting administrator to configure AUTH_KEY and SECURE_AUTH_KEY. Do not send these keys to support.','orderguard').'
'.esc_html__('OrderGuard’s new erasure service is not ready on this site because its settings or its connection to OrderGuard are missing or refused. Customer erasures are not complete; contact OrderGuard support before switching erasure systems.','orderguard').'