08f9e313e0
- TMDO_Type_Caster: text / textarea values that are arrays or objects are stored with maybe_serialize() (was (string) → the literal "Array", which bridge-restore-native then wrote back over native meta); from_db() unserializes them with TMDO_Safe_Unserialize. text columns are VARCHAR(255): longer values are stored NULL (native keeps them) and logged as text_overflow. - Add `wp tmdo repair-array-literals [--entity] [--confirm]` (TMDO_Array_Literal_Repair): rewrites flat "Array" values from native meta and lists native rows that are "Array" themselves (backup needed). - Verify: TypeCasterSerializeTest 13 OK, ArrayLiteralRepairTest 3 OK; unit 685 OK, integration 483 OK, phpcs (phpcs.xml) 0, stan OK; live `serialized` 3 PASS; dev36 repair dry-run lists the value corrupted by the 2026-09-26 round trip (user #1 community-events-location) - PR: PR-055
112 lines
3.7 KiB
PHP
112 lines
3.7 KiB
PHP
<?php
|
|
/**
|
|
* TMDO_Array_Literal_Repair — finds flat text / textarea values stored as the
|
|
* literal "Array" (pre-1.1.0 type caster cast arrays with (string)) and
|
|
* rewrites them from native meta.
|
|
*
|
|
* Native rows that are themselves "Array" (written back by an earlier
|
|
* bridge-restore-native) cannot be repaired here: they are only listed, to be
|
|
* restored from a backup.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery -- table / column names come from the registry and adapter; values are prepared.
|
|
|
|
/**
|
|
* Repairs "Array" literals in flat text / textarea columns.
|
|
*/
|
|
final class TMDO_Array_Literal_Repair {
|
|
|
|
/** What (string) array produced. */
|
|
private const LITERAL = 'Array';
|
|
|
|
/**
|
|
* Scan (and optionally repair) one entity type.
|
|
*
|
|
* @param string $entity_type Entity type.
|
|
* @param bool $apply Write repairs; false = report only.
|
|
* @return array{found:int, repairable:int, repaired:int, unrepairable:array<int, array{group:string, key:string, id:int, reason:string}>, native_polluted:array<int, array{group:string, key:string, id:int}>}
|
|
*/
|
|
public static function run( string $entity_type, bool $apply ): array {
|
|
global $wpdb;
|
|
|
|
$report = array(
|
|
'found' => 0,
|
|
'repairable' => 0,
|
|
'repaired' => 0,
|
|
'unrepairable' => array(),
|
|
'native_polluted' => array(),
|
|
);
|
|
|
|
$adapter = TMDO_Entity_Registry::get_adapter( $entity_type );
|
|
if ( ! $adapter ) {
|
|
return $report;
|
|
}
|
|
$meta_table = $adapter->get_native_meta_table();
|
|
$id_col = $adapter->get_entity_id_column();
|
|
$meta_pk = 'user' === $entity_type ? 'umeta_id' : 'meta_id';
|
|
|
|
foreach ( TMDO_Entity_Registry::get_groups_for_type( $entity_type ) as $group ) {
|
|
$table = TMDO_Schema_Manager::get_table_name( $entity_type, $group );
|
|
if ( ! TMDO_Schema_Manager::table_exists( $table ) ) {
|
|
continue;
|
|
}
|
|
|
|
foreach ( TMDO_Entity_Registry::get_group_fields( $entity_type, $group ) as $field ) {
|
|
if ( ! in_array( $field['type'] ?? 'text', array( 'text', 'textarea' ), true ) ) {
|
|
continue;
|
|
}
|
|
$key = (string) $field['key'];
|
|
$col = TMDO_Schema_Manager::sanitize_column_name( $key );
|
|
|
|
$ids = $wpdb->get_col( $wpdb->prepare( "SELECT `{$id_col}` FROM `{$table}` WHERE `{$col}` = %s", self::LITERAL ) );
|
|
foreach ( $ids as $id ) {
|
|
$id = (int) $id;
|
|
++$report['found'];
|
|
|
|
$native = $wpdb->get_var( $wpdb->prepare( "SELECT meta_value FROM `{$meta_table}` WHERE `{$id_col}` = %d AND meta_key = %s ORDER BY `{$meta_pk}` LIMIT 1", $id, $key ) );
|
|
$reason = null === $native ? 'no_native_row' : ( self::LITERAL === $native ? 'native_is_array_literal' : '' );
|
|
if ( '' !== $reason ) {
|
|
$report['unrepairable'][] = array(
|
|
'group' => $group,
|
|
'key' => $key,
|
|
'id' => $id,
|
|
'reason' => $reason,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
++$report['repairable'];
|
|
if ( $apply ) {
|
|
$value = TMDO_Type_Caster::to_db( TMDO_Safe_Unserialize::run( $native ), $field + array( 'group' => $group ) );
|
|
$wpdb->update( $table, array( $col => $value ), array( $id_col => $id ) );
|
|
++$report['repaired'];
|
|
}
|
|
}
|
|
|
|
$polluted = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT `{$id_col}` FROM `{$meta_table}` WHERE meta_key = %s AND meta_value = %s", $key, self::LITERAL ) );
|
|
foreach ( $polluted as $id ) {
|
|
$report['native_polluted'][] = array(
|
|
'group' => $group,
|
|
'key' => $key,
|
|
'id' => (int) $id,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( $report['repaired'] > 0 ) {
|
|
TMDO_Cache_Orchestrator::flush_entity( $entity_type );
|
|
}
|
|
|
|
return $report;
|
|
}
|
|
}
|