array( array('install_id'=>'char(32)','namespace'=>'char(32)','kind'=>'varchar(9)', 'subject_hash'=>'char(64)','cutoff_seconds'=>'bigint unsigned'), array('PRIMARY'=>array(0,array('install_id','namespace','kind','subject_hash'))), array('cutoff_seconds'))); } /** Disposable fresh-addon installer, not an upgrade or activation hook. * No historical readiness bit is created or enabled. DDL is non-atomic. */ public function installHistoricalSchema(): array { try { $this->connect(1);$this->validateSchema();$this->validateBlocks();$this->validateCases(); $existing=$this->rows('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME=?', array($this->profile['database'],$this->prefix.'historical_fences')); if($existing){throw new OG_Privacy_Case_Refusal('historical_schema_exists');} $this->sql('CREATE TABLE '.$this->table('historical_fences').' (install_id CHAR(32) NOT NULL,namespace CHAR(32) NOT NULL,kind VARCHAR(9) NOT NULL,subject_hash CHAR(64) NOT NULL,cutoff_seconds BIGINT UNSIGNED NULL,PRIMARY KEY(install_id,namespace,kind,subject_hash)) ENGINE=InnoDB DEFAULT CHARSET=ascii COLLATE=ascii_bin'); $this->validateCases(self::historicalSpecs()); return array('status'=>'historical_schema_installed','historical_complete'=>false,'dispatch_enabled'=>false); }catch(Throwable $e){return $this->failure($e,'historical_install');}finally{$this->finish();} } private static function historicalInput(array $context,array $hashes,$orderRef): array { foreach(array('install','generation','suppression_namespace') as $key){self::token($context[$key]??'');} if(!array_is_list($hashes) || count($hashes)>24 || count(array_unique($hashes,SORT_REGULAR))!==count($hashes)){ throw new OG_Privacy_Case_Refusal('historical_identity_invalid'); } foreach($hashes as $hash){if(!is_string($hash) || !preg_match('/^[a-f0-9]{64}$/D',$hash)){throw new OG_Privacy_Case_Refusal('historical_identity_invalid');}} // This version covers canonical positive WordPress order IDs only. // Other server reference formats require a separate adapter, not casts. if($orderRef!==null && (!is_string($orderRef) || !preg_match('/^[1-9][0-9]{0,18}$/D',$orderRef) || (strlen($orderRef)===19 && strcmp($orderRef,'9223372036854775807')>0))){throw new OG_Privacy_Case_Refusal('historical_reference_invalid');} if(!$hashes && $orderRef===null){throw new OG_Privacy_Case_Refusal('historical_identity_invalid');} $keys=array();foreach($hashes as $hash){$keys[]=array('identity',$hash);} if($orderRef!==null){$keys[]=array('order_ref',hash('sha256',"og-historical-order-ref-v1\n".$orderRef));} return $keys; } private static function historicalTimestamp($value): void { if($value!==null && (!is_int($value) || $value<0)){throw new OG_Privacy_Case_Refusal('historical_time_invalid');} } private static function historicalStoredCutoff($value): ?int { if($value===null){return null;} // Native prepared results may be integers; text protocol uses strings. if(is_int($value) && $value>=0){return $value;} if(!is_string($value) || !preg_match('/^(0|[1-9][0-9]{0,18})$/D',$value) || (strlen($value)===19 && strcmp($value,'9223372036854775807')>0)){ throw new OG_Privacy_Case_Refusal('historical_row_invalid'); } return (int)$value; } private function historicalEnter(array $context,bool $exclusive,array $extraSpecs=array()): void { $this->connect(1);$this->validateCases(array_merge(self::subjectSpecs(),self::historicalSpecs(),$extraSpecs)); $control=$this->enter($context['install'],$context['generation'],$exclusive); if($control['state']!=='open'){throw new OG_Privacy_Case_Refusal('closed');} $state=$this->rows('SELECT install_id,generation,suppression_namespace FROM '.$this->table('subject_authority_state').' WHERE singleton=1'); if(count($state)!==1 || $state[0]['install_id']!==$context['install'] || $state[0]['generation']!==$context['generation'] || $state[0]['suppression_namespace']!==$context['suppression_namespace']){throw new OG_Privacy_Case_Refusal('historical_binding_changed');} } /** Trusted caller asserts authority to preserve this fence, NOT remote success. * NULL cutoff means all time. A reference fence is always permanent. * Preserve greatest cutoff; never rewrite an all-time fence to a dated one. */ public function recordHistoricalFence(array $context,array $hashes,$orderRef,$cutoffSeconds,$callerAssertsFenceAuthorised=false): array { $phase='validate'; try { $keys=self::historicalInput($context,$hashes,$orderRef);self::historicalTimestamp($cutoffSeconds); if($callerAssertsFenceAuthorised!==true){throw new OG_Privacy_Case_Refusal('historical_authority_not_asserted');} $phase='admit';$this->historicalEnter($context,true); $phase='historical_rows';$changed=$this->historicalWriteKeys($context,$keys,$cutoffSeconds); $phase='commit';$this->commit(); return array('status'=>'historical_fence_saved','changed'=>$changed,'historical_complete'=>false,'erasure_complete'=>false,'dispatch_enabled'=>false); }catch(Throwable $e){return $this->failure($e,$phase);}finally{$this->finish();} } /** Internal only: caller must already hold the exclusive lifecycle gate. */ private function historicalWriteKeys(array $context,array $keys,$cutoffSeconds): int { $clock=$this->rows('SELECT UNIX_TIMESTAMP() AS now_seconds'); if(count($clock)!==1){throw new OG_Privacy_Case_Refusal('historical_clock_invalid');} $now=self::historicalStoredCutoff((string)$clock[0]['now_seconds']); if($now===null || ($cutoffSeconds!==null && $cutoffSeconds>$now+300)){throw new OG_Privacy_Case_Refusal('historical_cutoff_future');} $changed=0; foreach($keys as [$kind,$hash]){ $binding=array($context['install'],$context['suppression_namespace'],$kind,$hash); $old=$this->rows('SELECT cutoff_seconds FROM '.$this->table('historical_fences').' WHERE install_id=? AND namespace=? AND kind=? AND subject_hash=? LIMIT 2',$binding); if(count($old)>1){throw new OG_Privacy_Case_Refusal('historical_row_invalid');} $next=$kind==='order_ref'?null:$cutoffSeconds; if(!$old){ $changed+=$this->sql('INSERT INTO '.$this->table('historical_fences').' (install_id,namespace,kind,subject_hash,cutoff_seconds) VALUES (?,?,?,?,?)',array_merge($binding,array($next))); }else{ $previous=self::historicalStoredCutoff($old[0]['cutoff_seconds']); if($kind==='order_ref' && $previous!==null){throw new OG_Privacy_Case_Refusal('historical_row_invalid');} if($previous!==null && ($next===null || $next>$previous)){ if($this->sql('UPDATE '.$this->table('historical_fences').' SET cutoff_seconds=? WHERE install_id=? AND namespace=? AND kind=? AND subject_hash=?',array_merge(array($next),$binding))!==1){throw new OG_Privacy_Case_Refusal('historical_write_conflict');} $changed++; } } } return $changed; } /** Caller supplies captured ORIGINAL source time, not current editable time. * This primitive does not authenticate that evidence. A negative local * lookup NEVER becomes clear; server historical coverage is still unknown. * Shared lock ends on return: not an atomic check-and-send admission API. */ public function historicalProtection(array $context,array $hashes,$orderRef,$originalCreatedSeconds): array { try { $keys=self::historicalInput($context,$hashes,$orderRef);self::historicalTimestamp($originalCreatedSeconds); $this->historicalEnter($context,false); $where=array();$args=array($context['install'],$context['suppression_namespace']); foreach($keys as [$kind,$hash]){$where[]='(kind=? AND subject_hash=?)';$args[]=$kind;$args[]=$hash;} $rows=$this->rows('SELECT kind,subject_hash,cutoff_seconds FROM '.$this->table('historical_fences').' WHERE install_id=? AND namespace=? AND ('.implode(' OR ',$where).') LIMIT 26',$args); if(count($rows)>count($keys)){throw new OG_Privacy_Case_Refusal('historical_row_invalid');} $protected=false; foreach($rows as $row){ if(!in_array(array($row['kind'],$row['subject_hash']),$keys,true)){throw new OG_Privacy_Case_Refusal('historical_row_invalid');} $cutoff=self::historicalStoredCutoff($row['cutoff_seconds']); if($row['kind']==='order_ref' && $cutoff!==null){throw new OG_Privacy_Case_Refusal('historical_row_invalid');} if($row['kind']==='order_ref' || $cutoff===null || $originalCreatedSeconds===null || $originalCreatedSeconds<=$cutoff){$protected=true;} } $this->commit(); return array('status'=>$protected?'protected':'unknown','reason'=>$protected?'historical_match':'historical_coverage_unproven', 'historical_complete'=>false,'erasure_complete'=>false,'dispatch_enabled'=>false); }catch(Throwable $e){return array('status'=>'unknown','reason'=>$e instanceof OG_Privacy_Case_Refusal?$e->getMessage():'historical_read_unavailable', 'historical_complete'=>false,'erasure_complete'=>false,'dispatch_enabled'=>false);}finally{$this->finish();} } }