fix(2meet-data-optimizer): serialize arrays in text fields instead of storing "Array"
- 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
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* text / textarea fields holding arrays (e.g. usermeta wp_persisted_preferences,
|
||||
* community-events-location) were cast with (string) and stored as "Array";
|
||||
* restoring native meta then wrote "Array" back over the real data.
|
||||
*/
|
||||
class TypeCasterSerializeTest extends TestCase {
|
||||
|
||||
/** Rows passed to $wpdb->insert() (wpdo_errors via TMDO_Logger::warning). */
|
||||
public static array $inserts = array();
|
||||
|
||||
private const TEXT = array( 'type' => 'text', 'group' => 'admin_prefs', 'key' => 'community-events-location' );
|
||||
private const TEXTAREA = array( 'type' => 'textarea', 'group' => 'admin_prefs', 'key' => 'wp_persisted_preferences' );
|
||||
|
||||
protected function setUp(): void {
|
||||
self::$inserts = array();
|
||||
ini_set( 'error_log', '/dev/null' );
|
||||
|
||||
global $wpdb;
|
||||
$wpdb = new class {
|
||||
public string $prefix = 'wp_';
|
||||
public function insert( string $table, array $data, $format = null ) {
|
||||
TypeCasterSerializeTest::$inserts[] = array( $table, $data );
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static function arrays(): array {
|
||||
return array(
|
||||
'flat' => array( array( 'ip' => '118.163.181.0' ) ),
|
||||
'nested' => array( array( 'core/edit-post' => array( 'welcomeGuide' => false, 'panels' => array( 'a', 'b' ) ) ) ),
|
||||
'empty' => array( array() ),
|
||||
'list' => array( array( 1, 2, 3 ) ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider arrays
|
||||
*/
|
||||
public function test_text_array_round_trips( array $value ): void {
|
||||
$stored = TMDO_Type_Caster::to_db( $value, self::TEXT );
|
||||
|
||||
$this->assertNotSame( 'Array', $stored );
|
||||
$this->assertSame( serialize( $value ), $stored );
|
||||
$this->assertSame( $value, TMDO_Type_Caster::from_db( $stored, self::TEXT ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider arrays
|
||||
*/
|
||||
public function test_textarea_array_round_trips( array $value ): void {
|
||||
$stored = TMDO_Type_Caster::to_db( $value, self::TEXTAREA );
|
||||
|
||||
$this->assertSame( serialize( $value ), $stored );
|
||||
$this->assertSame( $value, TMDO_Type_Caster::from_db( $stored, self::TEXTAREA ) );
|
||||
}
|
||||
|
||||
public function test_object_is_serialized_not_cast(): void {
|
||||
$value = (object) array( 'a' => 1 );
|
||||
|
||||
$this->assertSame( serialize( $value ), TMDO_Type_Caster::to_db( $value, self::TEXTAREA ) );
|
||||
}
|
||||
|
||||
public function test_plain_strings_are_unchanged(): void {
|
||||
foreach ( array( 'fresh', '', 'a:b', '0' ) as $s ) {
|
||||
$this->assertSame( $s, TMDO_Type_Caster::to_db( $s, self::TEXT ) );
|
||||
$this->assertSame( $s, TMDO_Type_Caster::from_db( $s, self::TEXT ) );
|
||||
}
|
||||
$this->assertSame( '42', TMDO_Type_Caster::to_db( 42, self::TEXT ) );
|
||||
}
|
||||
|
||||
public function test_text_over_255_chars_is_stored_as_null_and_logged(): void {
|
||||
$long = str_repeat( 'é', 256 );
|
||||
|
||||
$this->assertNull( TMDO_Type_Caster::to_db( $long, self::TEXT ) );
|
||||
$this->assertCount( 1, self::$inserts );
|
||||
$this->assertStringContainsString( 'text_overflow', self::$inserts[0][1]['hook'] );
|
||||
$this->assertStringContainsString( 'community-events-location', (string) self::$inserts[0][1]['context'] );
|
||||
}
|
||||
|
||||
public function test_text_of_exactly_255_chars_is_kept(): void {
|
||||
$max = str_repeat( 'é', 255 );
|
||||
|
||||
$this->assertSame( $max, TMDO_Type_Caster::to_db( $max, self::TEXT ) );
|
||||
$this->assertCount( 0, self::$inserts );
|
||||
}
|
||||
|
||||
public function test_serialized_array_over_255_chars_overflows_too(): void {
|
||||
$value = array( 'k' => str_repeat( 'x', 300 ) );
|
||||
|
||||
$this->assertNull( TMDO_Type_Caster::to_db( $value, self::TEXT ) );
|
||||
$this->assertSame( serialize( $value ), TMDO_Type_Caster::to_db( $value, self::TEXTAREA ), 'textarea is TEXT, no 255 limit' );
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user