array( array('install_id'=>'char(32)','namespace'=>'char(32)','request_id'=>'bigint unsigned','reason'=>'varchar(32)','recorded_at'=>'bigint unsigned'), array('PRIMARY'=>array(0,array('install_id','namespace','request_id','reason'))))); } /** Per-operation copy of each warning (table installed with the restart schema). */ private static function operationAttentionSpecs(): array { return array('native_operation_attention_v1'=>array(array('install_id'=>'char(32)','namespace'=>'char(32)','request_id'=>'bigint unsigned','operation_id'=>'char(64)','reason'=>'varchar(32)','recorded_at'=>'bigint unsigned'), array('PRIMARY'=>array(0,array('install_id','namespace','request_id','operation_id','reason'))))); } private function operationAttentionInsert(array $context,int $requestId,string $operation,string $reason,int $now): void { $args=array($context['install'],$context['suppression_namespace'],$requestId,$operation,$reason); if(!$this->rows('SELECT reason FROM '.$this->table('native_operation_attention_v1').' WHERE install_id=? AND namespace=? AND request_id=? AND operation_id=? AND reason=? LIMIT 1',$args)){ if($this->sql('INSERT INTO '.$this->table('native_operation_attention_v1').' (install_id,namespace,request_id,operation_id,reason,recorded_at) VALUES (?,?,?,?,?,?)',array_merge($args,array($now)))!==1){throw new OG_Privacy_Case_Refusal('native_attention_conflict');} } } /** Current operation for alert labelling only; never erasure authority. */ private function attentionOperation(array $context,int $requestId): ?string { $tables=$this->rows('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME IN (?,?)',array($this->profile['database'],$this->prefix.'native_remote_operations_v4',$this->prefix.'native_operation_attention_v1')); if(count($tables)!==2){return null;} $this->validateTableSpecs(self::operationAttentionSpecs()); $rows=$this->rows('SELECT operation_id FROM '.$this->table('native_remote_operations_v4').' WHERE install_id=? AND namespace=? AND request_id=? LIMIT 2',array($context['install'],$context['suppression_namespace'],$requestId)); if(!$rows){return null;} if(count($rows)!==1 || !preg_match('/^[a-f0-9]{64}$/D',$rows[0]['operation_id'])){throw new OG_Privacy_Case_Refusal('native_attention_operation_invalid');} return $rows[0]['operation_id']; } private function operationAttentionRows(array $context,int $requestId): array { $tables=$this->rows('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME=?',array($this->profile['database'],$this->prefix.'native_operation_attention_v1')); if(!$tables){return array();} $this->validateTableSpecs(self::operationAttentionSpecs()); $rows=$this->rows('SELECT operation_id,reason,CAST(recorded_at AS CHAR) AS recorded_at FROM '.$this->table('native_operation_attention_v1').' WHERE install_id=? AND namespace=? AND request_id=? ORDER BY CAST(recorded_at AS UNSIGNED),operation_id,reason LIMIT 401',array($context['install'],$context['suppression_namespace'],$requestId)); if(count($rows)>400){throw new OG_Privacy_Case_Refusal('native_attention_invalid');} foreach($rows as &$row){ if(!preg_match('/^[a-f0-9]{64}$/D',$row['operation_id']) || !in_array($row['reason'],self::nativeAttentionReasons(),true)){throw new OG_Privacy_Case_Refusal('native_attention_invalid');} $row['recorded_at']=self::nativeAttemptInteger($row['recorded_at']); }unset($row); return $rows; } private static function attentionAuthoritySpecs(): array { return array('native_attention_authority'=>array( array('singleton'=>'tinyint unsigned','install_id'=>'char(32)','namespace'=>'char(32)','accepting'=>'tinyint unsigned'), array('PRIMARY'=>array(0,array('singleton'))))); } private static function attentionControlSpecs(): array { return array('control'=>array( array('singleton'=>'tinyint unsigned','install_id'=>'char(32)','generation'=>'char(32)','state'=>'varchar(8)','secret'=>'char(64)','schema_version'=>'smallint unsigned'), array('PRIMARY'=>array(0,array('singleton'))))); } /** Disposable addon only; non-atomic DDL, not an upgrade route. * Pin the namespace while the original subject source is still healthy. */ public function installNativeAttentionSchema(array $context): array { try { self::nativeAttentionInput($context,1); $this->connect(1);$this->validateSchema(); $r=$this->rows('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME IN (?,?)',array($this->profile['database'],$this->prefix.'native_erasure_attention',$this->prefix.'native_attention_authority')); if($r){throw new OG_Privacy_Case_Refusal('native_attention_schema_exists');} $this->sql('CREATE TABLE '.$this->table('native_erasure_attention').' (install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,request_id BIGINT UNSIGNED NOT NULL,reason VARCHAR(32) NOT NULL,recorded_at BIGINT UNSIGNED NOT NULL,PRIMARY KEY(install_id,namespace,request_id,reason)) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->sql('CREATE TABLE '.$this->table('native_attention_authority').' (singleton TINYINT UNSIGNED NOT NULL PRIMARY KEY,install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,accepting TINYINT UNSIGNED NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->validateCases(array_merge(self::nativeAttentionSpecs(),self::attentionAuthoritySpecs(),self::subjectSpecs())); $control=$this->enter($context['install'],$context['generation'],true); $source=$this->rows('SELECT install_id,generation,suppression_namespace FROM '.$this->table('subject_authority_state').' WHERE singleton=1'); if($control['state']!=='open' || count($source)!==1 || $source[0]['install_id']!==$context['install'] || $source[0]['generation']!==$context['generation'] || $source[0]['suppression_namespace']!==$context['suppression_namespace']){throw new OG_Privacy_Case_Refusal('attention_install_binding_changed');} if($this->sql('INSERT INTO '.$this->table('native_attention_authority').' (singleton,install_id,namespace,accepting) VALUES (1,?,?,1)',array($context['install'],$context['suppression_namespace']))!==1){throw new OG_Privacy_Case_Refusal('attention_install_unconfirmed');} $this->commit(); return array('status'=>'native_attention_schema_installed','erasure_complete'=>false); }catch(Throwable $e){return $this->failure($e,'native_attention_install');}finally{$this->finish();} } /** Diagnostic-only entry. Does not call enter()/historicalEnter(): those * require the very data schemas this channel reports failures about. * Closed permits diagnostics only; teardown must retire attention first. */ private function nativeAttentionEnter(array $context,bool $exclusive,bool $allowRetired=false): void { $this->connect(1); $this->validateTableSpecs(array_merge(self::attentionControlSpecs(),self::attentionAuthoritySpecs(),self::nativeAttentionSpecs())); $this->transactionControl('begin');$this->transaction=true; $control=$this->rows('SELECT install_id, generation, state, secret, schema_version FROM '.$this->table('control').' WHERE singleton = 1 '.($exclusive?'FOR UPDATE':'LOCK IN SHARE MODE')); $this->lockGranted=true; if(count($control)!==1 || $control[0]['install_id']!==$context['install'] || $control[0]['generation']!==$context['generation'] || (int)$control[0]['schema_version']!==1 || !in_array($control[0]['state'],array('open','closed'),true)){throw new OG_Privacy_Case_Refusal('attention_control_changed');} $authority=$this->rows('SELECT install_id,namespace,accepting FROM '.$this->table('native_attention_authority').' WHERE singleton=1'); if(count($authority)!==1 || $authority[0]['install_id']!==$context['install'] || $authority[0]['namespace']!==$context['suppression_namespace'] || !in_array((string)$authority[0]['accepting'],array('0','1'),true)){throw new OG_Privacy_Case_Refusal('attention_binding_changed');} if((string)$authority[0]['accepting']!=='1' && !$allowRetired){throw new OG_Privacy_Case_Refusal('attention_retired');} } /** Authorised teardown primitive: irreversible here, no data deletion. * Same gate as attention writers. Production uninstall must call it first. */ public function retireNativeAttention(array $context,$callerAssertsTeardownAuthorised=false): array { try { self::nativeAttentionInput($context,1); if($callerAssertsTeardownAuthorised!==true){throw new OG_Privacy_Case_Refusal('attention_teardown_not_asserted');} $this->nativeAttentionEnter($context,true,true); $this->sql('UPDATE '.$this->table('native_attention_authority').' SET accepting=0 WHERE singleton=1 AND accepting=1'); $this->commit();return array('status'=>'native_attention_retired','erasure_complete'=>false); }catch(Throwable $e){return $this->failure($e,'native_attention_retire');}finally{$this->finish();} } private static function nativeAttentionInput(array $context,$requestId): void { foreach(array('install','generation','suppression_namespace') as $key){self::token($context[$key]??'');} if(!is_int($requestId) || $requestId<1){throw new OG_Privacy_Case_Refusal('native_attention_input_invalid');} } private function nativeAttentionRows(array $context,int $requestId): array { $limit=count(self::nativeAttentionReasons()); $rows=$this->rows('SELECT reason,recorded_at FROM '.$this->table('native_erasure_attention').' WHERE install_id=? AND namespace=? AND request_id=? ORDER BY reason LIMIT '.($limit+1),array($context['install'],$context['suppression_namespace'],$requestId)); if(count($rows)>$limit){throw new OG_Privacy_Case_Refusal('native_attention_invalid');} foreach($rows as $row){ if(!in_array($row['reason'],self::nativeAttentionReasons(),true) || self::historicalStoredCutoff($row['recorded_at'])<1){throw new OG_Privacy_Case_Refusal('native_attention_invalid');} } return $rows; } /** Journal caller already holds the exclusive lifecycle lock. Append in * that SAME transaction: an attention transition cannot commit alone. * Independent of journal refresh; not tamper-proof against a database admin. */ private function nativeCoreRecordAttention(array $context,int $requestId,string $reason,int $now): void { if(!in_array($reason,array('core_store_domain_mismatch','core_proof_attention','core_proof_exhausted'),true)||$now<1){throw new OG_Privacy_Case_Refusal('core_attention_invalid');} $authority=$this->rows('SELECT install_id,namespace,accepting FROM '.$this->table('native_attention_authority').' WHERE singleton=1'); if(count($authority)!==1||$authority[0]['install_id']!==$context['install']||$authority[0]['namespace']!==$context['suppression_namespace']||(string)$authority[0]['accepting']!=='1'){throw new OG_Privacy_Case_Refusal('attention_binding_changed');} $rows=$this->nativeAttentionRows($context,$requestId); if(!in_array($reason,array_column($rows,'reason'),true)){ if($this->sql('INSERT INTO '.$this->table('native_erasure_attention').' (install_id,namespace,request_id,reason,recorded_at) VALUES (?,?,?,?,?)',array($context['install'],$context['suppression_namespace'],$requestId,$reason,$now))!==1){throw new OG_Privacy_Case_Refusal('native_attention_conflict');} } if(!in_array($reason,array_column($this->nativeAttentionRows($context,$requestId),'reason'),true)){throw new OG_Privacy_Case_Refusal('native_attention_conflict');} // Same transaction: the alert is also kept against the current operation, // so a restarted request shows each operation's own warnings. $operation=$this->attentionOperation($context,$requestId); if($operation!==null){$this->operationAttentionInsert($context,$requestId,$operation,$reason,$now);} } /** Caller supplies authorisation; service gates before reaching this method. * Each reason is retained once. Retry cannot refresh its first-observed time. */ public function recordNativeAttention(array $context,$requestId,string $reason): array { try { self::nativeAttentionInput($context,$requestId); if(!in_array($reason,self::nativeAttentionReasons(),true)){throw new OG_Privacy_Case_Refusal('native_attention_input_invalid');} $this->nativeAttentionEnter($context,true); $rows=$this->nativeAttentionRows($context,$requestId); $clock=$this->rows('SELECT UNIX_TIMESTAMP() AS now_seconds'); $now=count($clock)===1?self::historicalStoredCutoff($clock[0]['now_seconds']):null; if($now===null || $now<1){throw new OG_Privacy_Case_Refusal('native_attention_clock_invalid');} if(!in_array($reason,array_column($rows,'reason'),true)){ if($this->sql('INSERT INTO '.$this->table('native_erasure_attention').' (install_id,namespace,request_id,reason,recorded_at) VALUES (?,?,?,?,?)',array($context['install'],$context['suppression_namespace'],$requestId,$reason,$now))!==1){throw new OG_Privacy_Case_Refusal('native_attention_conflict');} } $verified=$this->nativeAttentionRows($context,$requestId); if(!in_array($reason,array_column($verified,'reason'),true)){throw new OG_Privacy_Case_Refusal('native_attention_conflict');} $operation=$this->attentionOperation($context,$requestId); if($operation!==null){$this->operationAttentionInsert($context,$requestId,$operation,$reason,$now);} $this->commit(); return array('status'=>'native_attention_recorded','automatic_retry'=>false,'erasure_complete'=>false); }catch(Throwable $e){return $this->failure($e,'native_attention');}finally{$this->finish();} } /** Empty evidence is never a completion/clearance claim. No core post lookup. */ public function readNativeAttention(array $context,$requestId): array { try { self::nativeAttentionInput($context,$requestId); $this->nativeAttentionEnter($context,false); $rows=$this->nativeAttentionRows($context,$requestId);$operations=$this->operationAttentionRows($context,$requestId);$this->commit(); return array('status'=>'native_attention_read','entries'=>$rows,'operations'=>$operations,'erasure_complete'=>false); }catch(Throwable $e){return $this->failure($e,'native_attention_read');}finally{$this->finish();} } /** Bounded keyset discovery, including deleted core requests. Historical warnings, * not a list of unresolved cases or permission to resume/send. */ public function listNativeAttention(array $context,int $after=0,int $limit=25): array { try { self::nativeAttentionInput($context,1); if($after<0 || $limit<1 || $limit>50){throw new OG_Privacy_Case_Refusal('native_attention_input_invalid');} $this->nativeAttentionEnter($context,false); $ids=$this->rows('SELECT DISTINCT request_id FROM '.$this->table('native_erasure_attention').' WHERE install_id=? AND namespace=? AND request_id>? ORDER BY request_id LIMIT '.($limit+1),array($context['install'],$context['suppression_namespace'],$after)); $more=count($ids)>$limit;$ids=array_slice($ids,0,$limit);$entries=array();$cursor=$after; foreach($ids as $row){$id=self::nativeAttemptInteger((string)$row['request_id']);$entries[]=array('request_id'=>$id,'warnings'=>$this->nativeAttentionRows($context,$id),'operations'=>$this->operationAttentionRows($context,$id));$cursor=$id;} $this->commit();return array('status'=>'native_attention_page','entries'=>$entries,'next_cursor'=>$cursor,'has_more'=>$more,'erasure_complete'=>false); }catch(Throwable $e){return $this->failure($e,'native_attention_page');}finally{$this->finish();} } }