array( array('install_id'=>'char(32)','namespace'=>'char(32)','request_id'=>'bigint unsigned','payload'=>'mediumtext','digest'=>'char(64)'), array('PRIMARY'=>array(0,array('install_id','namespace','request_id'))))); } public function installNativeScopeSchema(): array { try { $this->connect(1);$this->validateSchema(); $r=$this->rows('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME=?',array($this->profile['database'],$this->prefix.'native_scope_candidates')); if($r){throw new OG_Privacy_Case_Refusal('native_scope_schema_exists');} $this->sql('CREATE TABLE '.$this->table('native_scope_candidates').' (install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,request_id BIGINT UNSIGNED NOT NULL,payload MEDIUMTEXT NOT NULL,digest CHAR(64) NOT NULL,PRIMARY KEY(install_id,namespace,request_id)) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->validateTableSpecs(self::nativeScopeSpecs()); return array('status'=>'native_scope_schema_installed','erasure_complete'=>false); }catch(Throwable $e){return $this->failure($e,'scope_install');}finally{$this->finish();} } private static function scopeOrders(array $orders,string $emailHash): array { if(!array_is_list($orders) || count($orders)>25){throw new OG_Privacy_Case_Refusal('native_scope_capacity');} $seen=array(); foreach($orders as $row){ if(!is_array($row) || !in_array(array_keys($row),array(array('ref','order_key_hash','observed_created_seconds','identity_hashes'),array('ref','order_key_hash','observed_created_seconds','identity_hashes','source_roles')),true) || !is_string($row['ref']) || !preg_match('/^[1-9][0-9]{0,18}$/D',$row['ref']) || (strlen($row['ref'])===19 && strcmp($row['ref'],'9223372036854775807')>0) || isset($seen[$row['ref']]) || !is_string($row['order_key_hash']) || !preg_match('/^[a-f0-9]{64}$/D',$row['order_key_hash']) || ($row['observed_created_seconds']!==null && (!is_int($row['observed_created_seconds']) || $row['observed_created_seconds']<1)) || // Observed order evidence: two email hashes plus up to seven phone spellings. !is_array($row['identity_hashes']) || !array_is_list($row['identity_hashes']) || count($row['identity_hashes'])>9 || !in_array($emailHash,$row['identity_hashes'],true)) {throw new OG_Privacy_Case_Refusal('native_scope_invalid');} $hashes=$row['identity_hashes']; foreach($hashes as $hash){if(!is_string($hash) || !preg_match('/^[a-f0-9]{64}$/D',$hash)){throw new OG_Privacy_Case_Refusal('native_scope_invalid');}} sort($hashes,SORT_STRING); if($hashes!==$row['identity_hashes'] || count(array_unique($hashes))!==count($hashes)){throw new OG_Privacy_Case_Refusal('native_scope_invalid');} if(array_key_exists('source_roles',$row)){self::scopeRoles($row,$emailHash);} $seen[$row['ref']]=true; } usort($orders,static function($a,$b){return strlen($a['ref'])<=>strlen($b['ref']) ?: strcmp($a['ref'],$b['ref']);}); return $orders; } /** Legacy flat rows stay readable but may not acquire guessed source roles. */ private static function scopeRoles(array $row,string $emailHash): void { $r=$row['source_roles']??null;$h=$row['identity_hashes']; if(!is_array($r) || array_keys($r)!==array('request_email','billing_email','billing_phone') || !is_int($r['request_email']) || ($h[$r['request_email']]??null)!==$emailHash || ($r['billing_email']!==null && (!is_int($r['billing_email']) || !isset($h[$r['billing_email']]))) || !is_array($r['billing_phone']) || !array_is_list($r['billing_phone']) || count($r['billing_phone'])>7){throw new OG_Privacy_Case_Refusal('native_scope_roles_invalid');} $covered=array($r['request_email']);if($r['billing_email']!==null){$covered[]=$r['billing_email'];} $last=-1;foreach($r['billing_phone'] as $i){if(!is_int($i) || $i<0 || $i<=$last || !isset($h[$i])){throw new OG_Privacy_Case_Refusal('native_scope_roles_invalid');}$covered[]=$i;$last=$i;} $covered=array_values(array_unique($covered));sort($covered,SORT_NUMERIC); if($covered!==array_keys($h)){throw new OG_Privacy_Case_Refusal('native_scope_roles_invalid');} } private function nativeScopeBinding(array $context,$requestId,array $extraSpecs=array()): array { self::nativeAttentionInput($context,$requestId); $this->historicalEnter($context,true,array_merge(self::nativeFenceSpecs(),self::nativeScopeSpecs(),$extraSpecs)); $rows=$this->rows('SELECT email_hash,cutoff_seconds FROM '.$this->table('native_erasure_bindings').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 2',array($context['install'],$context['suppression_namespace'],$requestId)); if(count($rows)!==1 || !preg_match('/^[a-f0-9]{64}$/D',$rows[0]['email_hash']) || ($cutoff=self::historicalStoredCutoff($rows[0]['cutoff_seconds']))===null || $cutoff<1){throw new OG_Privacy_Case_Refusal('native_scope_binding_missing');} return array('install'=>$context['install'],'namespace'=>$context['suppression_namespace'],'request_id'=>$requestId,'email_hash'=>$rows[0]['email_hash'],'cutoff_seconds'=>$cutoff); } private function nativeScopeRow(array $binding): ?array { $rows=$this->rows('SELECT OCTET_LENGTH(payload) AS size FROM '.$this->table('native_scope_candidates').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 2',array($binding['install'],$binding['namespace'],$binding['request_id'])); if(!$rows){return null;} if(count($rows)!==1 || (int)$rows[0]['size']>32768){throw new OG_Privacy_Case_Refusal('native_scope_damaged');} $row=$this->rows('SELECT payload,digest FROM '.$this->table('native_scope_candidates').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 2',array($binding['install'],$binding['namespace'],$binding['request_id']))[0]; if(!hash_equals(hash('sha256',$row['payload']),$row['digest'])){throw new OG_Privacy_Case_Refusal('native_scope_damaged');} try { $body=json_decode($row['payload'],true,16,JSON_THROW_ON_ERROR); if(!is_array($body) || array_keys($body)!==array('binding','orders') || $body['binding']!==$binding || !is_array($body['orders']) || self::scopeOrders($body['orders'],$binding['email_hash'])!==$body['orders'] || json_encode($body,JSON_THROW_ON_ERROR)!==$row['payload']){throw new OG_Privacy_Case_Refusal('native_scope_damaged');} }catch(Throwable $e){throw new OG_Privacy_Case_Refusal('native_scope_damaged');} return $body; } /** Trusted caller supplies observed WC evidence; no arbitrary SQL callback. */ public function saveNativeScope(array $context,$requestId,array $orders): array { try { // Bounds first, before a connection; full binding validation follows. if(!array_is_list($orders) || count($orders)>25){throw new OG_Privacy_Case_Refusal('native_scope_capacity');} $binding=$this->nativeScopeBinding($context,$requestId); $orders=self::scopeOrders($orders,$binding['email_hash']); $body=array('binding'=>$binding,'orders'=>$orders);$payload=json_encode($body,JSON_THROW_ON_ERROR); if(strlen($payload)>32768){throw new OG_Privacy_Case_Refusal('native_scope_capacity');} $old=$this->nativeScopeRow($binding); if($old!==null && $old!==$body){throw new OG_Privacy_Case_Refusal('native_scope_conflict');} if($old===null){ if($this->sql('INSERT INTO '.$this->table('native_scope_candidates').' (install_id,namespace,request_id,payload,digest) VALUES (?,?,?,?,?)',array($binding['install'],$binding['namespace'],$requestId,$payload,hash('sha256',$payload)))!==1){throw new OG_Privacy_Case_Refusal('native_scope_conflict');} if($this->nativeScopeRow($binding)!==$body){throw new OG_Privacy_Case_Refusal('native_scope_damaged');} } $this->commit();return array('status'=>'native_scope_saved','candidate'=>$body,'replayed'=>$old!==null,'scope_complete'=>false,'erasure_complete'=>false,'dispatch_enabled'=>false); }catch(Throwable $e){return $this->failure($e,'scope_save');}finally{$this->finish();} } public function readNativeScope(array $context,$requestId): array { try { $binding=$this->nativeScopeBinding($context,$requestId);$body=$this->nativeScopeRow($binding);$this->commit(); return array('status'=>$body===null?'native_scope_missing':'native_scope_saved','candidate'=>$body,'scope_complete'=>false,'erasure_complete'=>false,'dispatch_enabled'=>false); }catch(Throwable $e){return $this->failure($e,'scope_read');}finally{$this->finish();} } }