prefix . 'chat4u_commerce'; } public static function activate(): void { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table=self::table(); dbDelta("CREATE TABLE {$table} ( record_key varchar(80) NOT NULL, kind varchar(24) NOT NULL, subject_hash char(64) NOT NULL DEFAULT '', cart_identity char(64) NOT NULL DEFAULT '', parameter_hash char(64) NOT NULL DEFAULT '', state varchar(24) NOT NULL DEFAULT '', payload longtext NULL, expires_at bigint unsigned NOT NULL, updated_at bigint unsigned NOT NULL, PRIMARY KEY (record_key), KEY expires_at (expires_at), KEY kind_state (kind,state), KEY cart_state (cart_identity,state), KEY subject_hash (subject_hash) ) " . $wpdb->get_charset_collate() . ';'); } public static function ensure(): void { if (Chat4U_Commerce::available() && get_option("chat4u_commerce_schema") !== "4") { self::activate();global $wpdb; $columns=$wpdb->get_col($wpdb->prepare("SHOW COLUMNS FROM %i",self::table())); if (!array_diff(["record_key","kind","subject_hash","cart_identity","parameter_hash","state","payload","expires_at","updated_at"],$columns)) { update_option("chat4u_commerce_schema","4",false); } } } private static function key(string $module): string { $name='chat4u_commerce_key_' . $module; $key=get_option($name); if (!$key) { add_option($name,bin2hex(random_bytes(32)),'',false); $key=get_option($name); } return hex2bin($key); } public static function encode(array $value,string $module): string { $nonce=random_bytes(12);$tag=''; $body=openssl_encrypt(wp_json_encode($value),'aes-256-gcm',self::key($module),OPENSSL_RAW_DATA,$nonce,$tag); if ($body===false) { throw new RuntimeException('Commerce encryption unavailable'); } return base64_encode($nonce.$tag.$body); } public static function decode(?string $value,string $module): array { $raw=base64_decode($value ?: '',true); if ($raw===false || strlen($raw)<28) { return []; } $body=openssl_decrypt(substr($raw,28),'aes-256-gcm',self::key($module),OPENSSL_RAW_DATA,substr($raw,0,12),substr($raw,12,16)); return $body===false ? [] : (array) json_decode($body,true); } public static function put(string $key,string $kind,array $value,int $seconds,string $module='cart'): void { global $wpdb; $ok=$wpdb->replace(self::table(),['record_key'=>$key,'kind'=>$kind,'subject_hash'=>$value['email_hash'] ?? $value['grant']['email_hash'] ?? '', 'payload'=>self::encode($value,$module), 'expires_at'=>time()+$seconds,'updated_at'=>time()]); if ($ok===false) { throw new RuntimeException('Commerce receipt unavailable'); } } public static function get(string $key,string $module='cart'): array { global $wpdb; $row=$wpdb->get_row($wpdb->prepare('SELECT payload,expires_at FROM %i WHERE record_key=%s',self::table(),$key),ARRAY_A); return $row && (int)$row['expires_at']>time() ? self::decode($row['payload'],$module) : []; } public static function cleanup(): array { if (get_option("chat4u_commerce_schema") !== "4") { return []; } Chat4U_Commerce_Privacy::cleanup(); global $wpdb; $wpdb->query($wpdb->prepare("DELETE FROM %i WHERE option_name LIKE %s AND CAST(option_value AS UNSIGNED)<=%d LIMIT 100",$wpdb->options,$wpdb->esc_like("chat4u_woo_nonce_")."%",time())); // Preserve unresolved action identity; scrub its sensitive payload separately. $wpdb->query($wpdb->prepare("UPDATE %i SET payload=NULL WHERE expires_at<=%d AND payload IS NOT NULL AND kind='action' AND state IN ('sending','unknown') LIMIT 100",self::table(),time())); $wpdb->query($wpdb->prepare("DELETE FROM %i WHERE expires_at<=%d AND kind<>'order_cleanup' AND NOT(kind='action' AND state IN ('sending','unknown')) LIMIT 100",self::table(),time())); return ['oldest_expired'=>(int)$wpdb->get_var($wpdb->prepare("SELECT MIN(expires_at) FROM %i WHERE expires_at<=%d AND (payload IS NOT NULL OR NOT(kind='action' AND state IN ('sending','unknown')))",self::table(),time()))]; } public static function revoke($prior=null,$next=null): void { if ($next===null || (!empty($prior["cart_assistance"]) && empty($next["cart_assistance"]))) { delete_option("chat4u_commerce_key_cart"); } if ($next===null || (!empty($prior["order_status"]) && empty($next["order_status"]))) { delete_option("chat4u_commerce_key_order"); } if ($next===null || (!empty($prior["purchase_evidence"]) && empty($next["purchase_evidence"]))) { delete_option("chat4u_commerce_key_measurement"); } self::cleanup(); } }