120) { $error = new \WP_Error(); $error->errors['cf_transaction_busy'] = array('Another CustomFields.com operation is already in progress. Finish it before starting another transaction.'); return $error; } try { $token = bin2hex(random_bytes(32)); } catch (\Throwable $exception) { $error = new \WP_Error(); $error->errors['cf_transaction_random'] = array('A protected operation could not be started. Retry after checking the server.'); return $error; } self::$transaction_owner = array('token' => $token, 'owner' => $owner); return $token; } /** Only the exact successful claimant may release ownership, including on errors. */ public static function leave_transaction($token) { if (!is_string($token) || self::$transaction_owner === null || !hash_equals(self::$transaction_owner['token'], $token)) { return false; } self::$transaction_owner = null; return true; } /** Contain interrupted rollback before the owner can release its transaction token. */ public static function rollback_transaction() { global $wpdb; $rolled_back = false; try { $rolled_back = false !== $wpdb->query('ROLLBACK'); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Database transaction/advisory-lock operation; WordPress has no equivalent API and lock results must never be cached. catch (\Throwable $exception) { /* Third-party query details must not escape cleanup. */ } if (!$rolled_back) { // Closing the connection discards its uncommitted writes. Do not reconnect here. try { $wpdb->close(); } catch (\Throwable $exception) { /* Keep the original operation's controlled result. */ } error_log('CustomFields.com storage: rollback was interrupted; the database connection was closed.'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Operational failure report with a fixed message and no private data; retaining it is necessary when rollback or post-commit cleanup fails. } return $rolled_back; } public static function boot() { add_filter('customfields_validate_group_schema',array(__CLASS__,'schema'),30,2); add_action('customfields_group_editor_settings',array(__CLASS__,'settings'),30); add_action('customfields_metadata_transaction_written',array(__CLASS__,'transaction_written'),10,5); add_action('admin_menu',array(__CLASS__,'menu'),26); foreach(array('post','user','term') as $kind){ add_filter('get_'.$kind.'_metadata',static function($check,$id,$name,$single)use($kind){return self::read_filter($check,$kind,$id,$name,$single);},20,4); add_filter('update_'.$kind.'_metadata',static function($check,$id,$name,$value,$previous)use($kind){return self::update_filter($check,$kind,$id,$name,$value,$previous);},20,5); add_filter('add_'.$kind.'_metadata',static function($check,$id,$name,$value,$unique)use($kind){return self::update_filter($check,$kind,$id,$name,$value,'',true);},20,5); add_filter('delete_'.$kind.'_metadata',static function($check,$id,$name,$value,$all)use($kind){return self::delete_filter($check,$kind,$id,$name,$value,$all);},20,5); } } public static function table(){global $wpdb;return $wpdb->prefix.'customfields_values';} public static function installed(){global $wpdb;return $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s',$wpdb->esc_like(self::table())))===self::table();} // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Inspect/provision the actual database schema; cached application metadata cannot establish storage-engine or column guarantees. public static function install(){ if(!current_user_can('manage_options'))return new \WP_Error('cf_storage_permission',__('Only administrators can provision storage.','customfields-com')); $transaction_token=self::enter_transaction(__METHOD__);if($transaction_token instanceof \WP_Error){return $transaction_token;} try{ global $wpdb;$table=self::table(); $sql="CREATE TABLE IF NOT EXISTS `$table` (generation varchar(32) NOT NULL, object_kind varchar(8) NOT NULL, object_id bigint unsigned NOT NULL, field_name varchar(64) NOT NULL, field_value longtext NOT NULL, PRIMARY KEY (generation,object_kind,object_id,field_name)) ENGINE=InnoDB ".$wpdb->get_charset_collate(); return $wpdb->query($sql)===false?new \WP_Error('cf_storage_install',__('The custom values table could not be created.','customfields-com')):true; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- Read authoritative workflow/metadata state for validation and conflict detection; cached values can miss concurrent edits or duplicate rows. sql is the fixed CREATE TABLE definition using the WordPress-prefixed table name and wpdb::get_charset_collate; no request fragments enter the schema statement. }finally{self::leave_transaction($transaction_token);} } public static function schema($group,$raw){ if(is_wp_error($group))return $group; $storage=$raw['storage']??'metadata'; if(!in_array($storage,array('metadata','custom_table'),true))return new \WP_Error('cf_storage_schema',__('Choose native metadata or custom table storage.','customfields-com')); $group['storage']=$storage;$group['storage_generation']=''; if($storage==='custom_table'){ if(!in_array($group['target'],array('post','user','term'),true))return new \WP_Error('cf_storage_target',__('Custom table storage supports post, user and term groups.','customfields-com')); if(!self::installed())return new \WP_Error('cf_storage_missing',__('Provision the table under CustomFields.com → Storage before opting in.','customfields-com')); $generation=$raw['storage_generation']??''; if($generation!==''&&(!is_string($generation)||!preg_match('/^[a-f0-9]{32}$/D',$generation)))return new \WP_Error('cf_storage_generation',__('The storage generation is invalid.','customfields-com')); // A new opt-in always starts from the current native mirror, never dormant table rows. $previous=null;foreach(get_option('customfields_groups',array()) as $item){if(($item['key']??'')===$group['key']){$previous=$item;break;}} if (($previous['storage']??'metadata') === 'custom_table' && !empty($previous['storage_generation'])) { // An active generation belongs to this registry entry, not to an imported file. $generation = $previous['storage_generation']; } else { // Validation can run twice before a schema write. Retain only identifiers // issued against this exact pre-write state during the current request. $binding = hash('sha256', wp_json_encode(array($group['key'], $previous))); if ($generation === '' || (self::$issued_generations[$generation] ?? null) !== $binding) { $generation = str_replace('-', '', wp_generate_uuid4()); self::$issued_generations[$generation] = $binding; } } $group['storage_generation']=$generation; } return $group; } public static function settings($group){ echo '
'.esc_html__('Value storage','customfields-com').'

'.esc_html__('Post, user and term groups can use the dedicated table. Native metadata stays synchronized for WordPress queries, revisions and returning to metadata. Existing values are read safely until backfilled. Provision the table in Storage first.','customfields-com').'

'; } private static function resolve($kind,$id,$name){ if(!is_string($name)||$name===''||$name[0]==='_')return null; if($kind==='user' && method_exists(Schema::class,'reserved_user_name') && Schema::reserved_user_name($name))return null; foreach(customfields_get_groups() as $group){ if (($group['target']??'post') !== $kind) { continue; } // Resolve names before object capabilities: user construction itself reads usermeta. foreach($group['fields'] as $field){ if($field['name']===$name && Relationships::group_matches($group,$kind,$id))return array('group'=>$group,'field'=>$field); } }return null; } private static function table_row($group,$kind,$id,$name,$lock=false){ global $wpdb;$table=self::table(); $row=$wpdb->get_row($wpdb->prepare("SELECT field_value FROM %i WHERE generation=%s AND object_kind=%s AND object_id=%d AND field_name=%s".($lock?' FOR UPDATE':''),$table,$group['storage_generation'],$kind,$id,$name),ARRAY_A); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared -- Read or lock authoritative rows for the transaction; an object-cache result cannot provide database locking or conflict detection. Query concatenation adds only the fixed FOR UPDATE suffix (or fixed snapshot-size column expressions); data values remain bound by prepare. if($wpdb->last_error)throw new \RuntimeException(__('Custom table storage could not be read.','customfields-com')); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception transport, not an HTML output sink; public handlers escape the resulting error at rendering time. return $row; } public static function read_filter($check,$kind,$id,$name,$single){ if($check!==null||self::$inside)return $check; $found=self::resolve($kind,$id,$name); if(!$found||($found['group']['storage']??'metadata')!=='custom_table')return $check; try{$row=self::table_row($found['group'],$kind,$id,$name);}catch(\Throwable $error){return $check;} // Both metadata_exists() and get_metadata() accept this wrapper, including false/empty values. return $row?array(maybe_unserialize($row['field_value'])):$check; } private static function meta_info($kind){global $wpdb;return array('table'=>$kind==='user'?$wpdb->usermeta:($kind==='term'?$wpdb->termmeta:$wpdb->postmeta),'object'=>$kind.'_id','primary'=>$kind==='user'?'umeta_id':'meta_id');} public static function locked_value($kind,$id,$name){ global $wpdb;$info=self::meta_info($kind); $rows=$wpdb->get_results($wpdb->prepare("SELECT %i,meta_value FROM %i WHERE %i=%d AND meta_key=%s ORDER BY %i FOR UPDATE", $info['primary'], $info['table'], $info['object'], $id, $name, $info['primary']),ARRAY_A); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Read or lock authoritative rows for the transaction; an object-cache result cannot provide database locking or conflict detection. if($wpdb->last_error||count($rows)>1)throw new \RuntimeException(__('The value is ambiguous or could not be locked. Nothing was changed.','customfields-com')); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception transport, not an HTML output sink; public handlers escape the resulting error at rendering time. $native=array('exists'=>(bool)$rows,'value'=>$rows?maybe_unserialize($rows[0]['meta_value']):null,'rows'=>$rows); $found=self::resolve($kind,$id,$name); if($found&&($found['group']['storage']??'metadata')==='custom_table'){ $row=self::table_row($found['group'],$kind,$id,$name,true); if($row){if(!$rows||$row['field_value']!==$rows[0]['meta_value'])throw new \RuntimeException(__('The table and compatibility mirror disagree. Restore consistency before editing.','customfields-com'));$native['value']=maybe_unserialize($row['field_value']);} // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception transport, not an HTML output sink; public handlers escape the resulting error at rendering time. } return $native; } /** Write native metadata and table within an existing transaction, preserving literal serialized text. */ public static function write_locked($group,$kind,$id,$name,$value){ global $wpdb;$info=self::meta_info($kind);$before=self::locked_value($kind,$id,$name); if($value===null){$ok=$wpdb->query($wpdb->prepare("DELETE FROM %i WHERE %i=%d AND meta_key=%s", $info['table'], $info['object'], $id, $name));} // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic workflow write with explicit formats/prepared values; surrounding transaction performs cache invalidation after commit. else{ $raw=maybe_serialize($value); $ok=$before['exists']?$wpdb->query($wpdb->prepare("UPDATE %i SET meta_value=%s WHERE %i=%d AND meta_key=%s", $info['table'], $raw, $info['object'], $id, $name)):$wpdb->insert($info['table'],array($info['object']=>$id,'meta_key'=>$name,'meta_value'=>$raw),array('%d','%s','%s')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Atomic workflow write with explicit formats/prepared values; surrounding transaction performs cache invalidation after commit. Metadata column name or presentation value, not an unbounded WP_Query meta scan. } if($ok===false)throw new \RuntimeException(__('Metadata could not be saved. Nothing was changed.','customfields-com')); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception transport, not an HTML output sink; public handlers escape the resulting error at rendering time. self::mirror($group,$kind,$id,$name,$value); self::$touched[$kind.':'.$id]=array($kind,$id); } private static function mirror($group,$kind,$id,$name,$value){ if(($group['storage']??'metadata')!=='custom_table')return; global $wpdb;$table=self::table(); if($value===null){$ok=$wpdb->query($wpdb->prepare("DELETE FROM %i WHERE generation=%s AND object_kind=%s AND object_id=%d AND field_name=%s", $table, $group['storage_generation'], $kind, $id, $name));} // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic workflow write with explicit formats/prepared values; surrounding transaction performs cache invalidation after commit. else{$ok=$wpdb->query($wpdb->prepare("INSERT INTO %i (generation,object_kind,object_id,field_name,field_value) VALUES (%s,%s,%d,%s,%s) ON DUPLICATE KEY UPDATE field_value=VALUES(field_value)", $table, $group['storage_generation'], $kind, $id, $name, maybe_serialize($value)));} // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic workflow write with explicit formats/prepared values; surrounding transaction performs cache invalidation after commit. if($ok===false)throw new \RuntimeException(__('The custom table could not be saved. Nothing was changed.','customfields-com')); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception transport, not an HTML output sink; public handlers escape the resulting error at rendering time. } public static function flush(){foreach(self::$touched as $object){wp_cache_delete($object[1],$object[0].'_meta');}self::$touched=array();wp_cache_delete('customfields_groups','options');wp_cache_delete('alloptions','options');wp_cache_delete('notoptions','options');} private static function transactional(){ global $wpdb;$tables=array($wpdb->options,$wpdb->postmeta,$wpdb->usermeta,$wpdb->termmeta);if(self::installed())$tables[]=self::table(); foreach($tables as $table){$engine=$wpdb->get_var($wpdb->prepare('SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s',$table));if(strtoupper((string)$engine)!=='INNODB')throw new \RuntimeException(__('Atomic content changes require InnoDB metadata, options and custom tables.','customfields-com'));} // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Inspect/provision the actual database schema; cached application metadata cannot establish storage-engine or column guarantees. Internal exception transport, not an HTML output sink; public handlers escape the resulting error at rendering time. } public static function save_group($group,$kind,$id,$updates){ if(!is_array($updates)||!Relationships::group_matches($group,$kind,$id)||!Editor::can_edit($kind,$id))return new \WP_Error('cf_storage_permission',__('You cannot save this group on this object.','customfields-com')); $known = array_column($group['fields'], null, 'name'); foreach ($updates as $name => $value) { if (!isset($known[$name]) || !Editor::can_edit($kind,$id,$name)) { return new \WP_Error('cf_storage_permission', __('Unknown or protected field.', 'customfields-com')); } } $atomic = ($group['storage'] ?? 'metadata') === 'custom_table'; foreach ($group['fields'] as $field) { if ($field['type'] === 'relationship' && array_key_exists($field['name'], $updates)) { $atomic = true; } } // Existing ordinary groups keep WordPress metadata hooks and storage-engine compatibility. if (!$atomic) { foreach ($updates as $name => $value) { if (!Editor::can_edit($kind, $id, $name)) { return new \WP_Error('cf_storage_permission', __('You cannot edit this field.', 'customfields-com')); } if ($value === null) { delete_metadata($kind, $id, $name); } else { update_metadata($kind, $id, $name, wp_slash($value)); } // Native metadata APIs return false for both unchanged values and failures. Verify the actual rows. global $wpdb; wp_cache_delete($id, $kind . '_meta'); $rows = $wpdb->get_col($wpdb->prepare('SELECT meta_value FROM %i WHERE %i=%d AND meta_key=%s', _get_meta_table($kind), $kind . '_id', $id, $name)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Read back authoritative persisted rows after native metadata hooks; cached or filtered metadata cannot distinguish unchanged success from a failed write. $verified = !$wpdb->last_error && ($value === null ? !$rows : (bool) $rows); if ($value !== null) { foreach ($rows as $row) { if ((string) $row !== (string) maybe_serialize($value)) { $verified = false; break; } } } if (!$verified) { return new \WP_Error('cf_storage_save', __('A field value could not be saved and verified. Earlier fields may have saved. Reload this editor and check the saved values before retrying.', 'customfields-com')); } } return true; } $transaction_token=self::enter_transaction(__METHOD__);if($transaction_token instanceof \WP_Error){return $transaction_token;} global $wpdb;$begun=false; try{ self::transactional(); if($wpdb->query('START TRANSACTION')===false)throw new \RuntimeException(__('The content transaction could not start.','customfields-com'));$begun=true; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Database transaction/advisory-lock operation; WordPress has no equivalent API and lock results must never be cached. if($wpdb->query($wpdb->prepare("SELECT option_value FROM {$wpdb->options} WHERE option_name=%s FOR UPDATE",'customfields_groups'))===false)throw new \RuntimeException(__('The schema could not be locked.','customfields-com')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Read or lock authoritative rows for the transaction; an object-cache result cannot provide database locking or conflict detection. wp_cache_delete('customfields_groups','options');wp_cache_delete('alloptions','options');$current=null; foreach(customfields_get_groups() as $item){if($item['key']===$group['key']){$current=$item;break;}} if(!$current||wp_json_encode($current)!==wp_json_encode($group))throw new \RuntimeException(__('The schema changed during editing. Reload before saving.','customfields-com')); $before=array();$fields=array_column($group['fields'],null,'name'); foreach($updates as $name=>$value){ if(!isset($fields[$name])||!Editor::can_edit($kind,$id,$name))throw new \RuntimeException(__('You cannot edit one of these fields.','customfields-com')); $row=self::locked_value($kind,$id,$name);$before[$name]=$row['exists']?$row['value']:array(); if($row['exists'] && is_wp_error(Fields::storage_error($row['value'],$fields[$name])))throw new \RuntimeException(__('Existing data needs a compatible schema or explicit migration.','customfields-com')); if($value!==null&&is_wp_error(Fields::storage_error($value,$fields[$name])))throw new \RuntimeException(__('A value is incompatible with its field schema.','customfields-com')); } Relationships::synchronize($group,$kind,$id,array_map(static function($value){return $value===null?array():$value;},$updates),$before); foreach($updates as $name=>$value)self::write_locked($group,$kind,$id,$name,$value); if($wpdb->query('COMMIT')===false)throw new \RuntimeException(__('The content transaction could not commit.','customfields-com')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Database transaction/advisory-lock operation; WordPress has no equivalent API and lock results must never be cached. $begun=false;return true; }catch(\Throwable $error){if($begun)self::rollback_transaction();return new \WP_Error('cf_storage_save',$error->getMessage());} finally{ try{self::flush();}catch(\Throwable $cleanup){error_log('CustomFields.com storage: cache cleanup failed. Inspect saved content before retrying.');} // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Operational failure report with a fixed message and no private data; retaining it is necessary when rollback or post-commit cleanup fails. finally{self::leave_transaction($transaction_token);} } } /** Direct SQL callers must invoke before COMMIT and call flush() after COMMIT/ROLLBACK. */ public static function transaction_written($group,$kind,$id,$updates,$before_rows){ $managed = ($group['storage'] ?? 'metadata') === 'custom_table'; foreach ($group['fields'] as $field) { if ($field['type'] === 'relationship' && array_key_exists($field['name'], $updates)) { $managed = true; } } // Other transaction owners retain their own storage-engine requirements. if (!$managed) { return; } self::transactional(); $before=array();foreach($updates as $name=>$value){ $rows=$before_rows[$name]??array(); if(count($rows)>1)throw new \RuntimeException(__('Duplicate metadata prevents a safe relationship save.','customfields-com')); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception transport, not an HTML output sink; public handlers escape the resulting error at rendering time. if (($group['storage']??'metadata')==='custom_table') { $table_row=self::table_row($group,$kind,$id,$name,true); if ($table_row && (!$rows || $table_row['field_value']!==$rows[0]['meta_value'])) { throw new \RuntimeException(__('The table and metadata mirror changed independently. Nothing was saved.','customfields-com')); } // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception transport, not an HTML output sink; public handlers escape the resulting error at rendering time. } $before[$name]=$rows?maybe_unserialize($rows[0]['meta_value']):array();self::$touched[$kind.':'.$id]=array($kind,$id); } Relationships::synchronize($group,$kind,$id,array_map(static function($value){return $value===null?array():$value;},$updates),$before); foreach($updates as $name=>$value)self::mirror($group,$kind,$id,$name,$value); } public static function update_filter($check,$kind,$id,$name,$value,$previous='',$add=false){ if($check!==null||self::$inside)return $check;$found=self::resolve($kind,$id,$name); if(!$found||(($found['group']['storage']??'metadata')!=='custom_table'&&$found['field']['type']!=='relationship'))return $check; $existing=get_metadata($kind,$id,$name,true); if(($add&&metadata_exists($kind,$id,$name))||($previous!==''&&$existing!==$previous))return false; $clean=Fields::sanitize($value,$found['field'],array('stored_value'=>$existing,'has_stored_value'=>metadata_exists($kind,$id,$name),'object_kind'=>$kind,'object_id'=>$id)); if(is_wp_error($clean))return false; return !is_wp_error(self::save_group($found['group'],$kind,$id,array($name=>$clean))); } public static function delete_filter($check,$kind,$id,$name,$value,$all){ if($check!==null||self::$inside)return $check; if($all){foreach(customfields_get_groups() as $group){if(($group['target']??'post')!==$kind)continue;foreach($group['fields'] as $field){if($field['name']===$name&&(($group['storage']??'metadata')==='custom_table'||$field['type']==='relationship'))return false;}}return $check;} $found=self::resolve($kind,$id,$name);if(!$found||(($found['group']['storage']??'metadata')!=='custom_table'&&$found['field']['type']!=='relationship'))return $check; if(!empty($found['field']['required'])||($value!==''&&get_metadata($kind,$id,$name,true)!==$value))return false; $result=self::save_group($found['group'],$kind,$id,array($name=>null));return !is_wp_error($result); } public static function menu(){add_submenu_page('customfields',__('Storage','customfields-com'),__('Storage','customfields-com'),'manage_options','customfields-storage',array(__CLASS__,'page'));} public static function backfill($group_key,$after=0){ if(!current_user_can('manage_options'))return new \WP_Error('cf_storage_permission',__('Only administrators can backfill values.','customfields-com')); $group=null;foreach(customfields_get_groups() as $candidate){if($candidate['key']===$group_key)$group=$candidate;} if(!$group||($group['storage']??'')!=='custom_table')return new \WP_Error('cf_storage_group',__('Choose a group using custom table storage.','customfields-com')); global $wpdb;$info=self::meta_info($group['target']);$fields=array_column($group['fields'],null,'name');$names=array_keys($fields); if(!$names)return array('cursor'=>$after,'count'=>0,'more'=>false); $placeholders=implode(',',array_fill(0,count($names),'%s')); $rows=$wpdb->get_results($wpdb->prepare("SELECT %i AS row_id,%i AS object_id,meta_key FROM %i WHERE %i>%d AND meta_key IN ($placeholders) ORDER BY %i LIMIT 100",array_merge(array($info['primary'],$info['object'],$info['table'],$info['primary'],$after),$names,array($info['primary']))),ARRAY_A); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Read authoritative workflow/metadata state for validation and conflict detection; cached values can miss concurrent edits or duplicate rows. Dynamic list contains only generated %s placeholders; the corresponding values and %i identifiers are passed in order to prepare via array_merge. if($wpdb->last_error)return new \WP_Error('cf_storage_read',__('The source metadata could not be read.','customfields-com')); $count=0;$cursor=$after; foreach($rows as $row){$cursor=(int)$row['row_id'];if(!Relationships::group_matches($group,$group['target'],(int)$row['object_id']))continue; $value=get_metadata($group['target'],$row['object_id'],$row['meta_key'],true); $result=self::save_group($group,$group['target'],(int)$row['object_id'],array($row['meta_key']=>$value));if(is_wp_error($result))return $result;$count++; } return array('cursor'=>$cursor,'count'=>$count,'more'=>count($rows)===100); } public static function page(){ if(!current_user_can('manage_options'))wp_die(esc_html__('Not permitted.','customfields-com')); $result=null; if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Exact HTTP method comparison; never stored or rendered. $nonce = isset($_POST['_wpnonce']) && is_string($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : ''; if (!wp_verify_nonce($nonce, 'customfields_storage')) { wp_die(esc_html__('Your session expired. Reload the storage page.', 'customfields-com'), '', array('response' => 403)); } if (isset($_POST['install'])) { $result = self::install(); } else { $cursor = isset($_POST['cursor']) && is_string($_POST['cursor']) ? sanitize_text_field(wp_unslash($_POST['cursor'])) : ''; if ($cursor === '' || $cursor !== wp_unslash($_POST['cursor']) || !preg_match('/^(0|[1-9][0-9]*)$/D', $cursor) || (string) (int) $cursor !== $cursor) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Second read only compares original bytes to the sanitized cursor; the typed sanitized value is the only value used. $result = new \WP_Error('cf_storage_cursor', __('Choose a valid nonnegative metadata row ID.', 'customfields-com')); } else { $group_key = isset($_POST['group']) && is_string($_POST['group']) ? sanitize_text_field(wp_unslash($_POST['group'])) : ''; $result = self::backfill($group_key, (int) $cursor); } } } echo '

'.esc_html__('Value storage','customfields-com').'

'.esc_html__('Opt individual post, user or term groups into the dedicated values table. A native metadata mirror remains mandatory for WordPress queries and revisions. Switching the group back to metadata uses this up-to-date mirror; no content is deleted. Direct SQL writes bypass this contract and are unsupported.','customfields-com').'

'; if(is_wp_error($result))echo '

'.esc_html($result->get_error_message()).'

'; elseif($result!==null)echo '

'.esc_html(is_array($result)?sprintf(/* translators: 1: Copied value count; 2: cursor position; 3: continuation message. */ __('Copied %1$d values. Cursor: %2$d. %3$s','customfields-com'),$result['count'],$result['cursor'],$result['more']?__('Continue from this cursor.','customfields-com'):__('This pass is complete.','customfields-com')):__('Table provisioned. Choose custom table storage in a field group.','customfields-com')).'

'; echo '
';wp_nonce_field('customfields_storage'); $table_groups = array_filter(customfields_get_groups(), static function ($group) { return ($group['storage'] ?? '') === 'custom_table'; }); if(!self::installed()){ echo '

'.esc_html__('WordPress metadata is the default and needs no setup here. Create this optional table only when you want to choose custom table storage for a field group. Creating the table does not move existing values.','customfields-com').'

'; submit_button(__('Create values table','customfields-com'),'primary','install'); } elseif (!$table_groups) { echo '

'.esc_html__('The values table is ready. No field groups use it yet. Edit a post, user or term group, open Storage & group options, and choose Custom table + compatibility mirror to use it.','customfields-com').'

'.esc_html__('Manage field groups','customfields-com').'

'; } else{echo '

'.esc_html__('Each explicit pass copies at most 100 metadata rows. Existing table values must agree with their mirror; failures stop the pass. Already copied rows remain valid.','customfields-com').'

';submit_button(__('Backfill next 100 rows','customfields-com'));} echo '
'; } }