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
100 lines
3.5 KiB
PHP
100 lines
3.5 KiB
PHP
<?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' );
|
|
}
|
|
}
|