Summary
lsx\legacy\Admin::remove_connected_id() calls array_diff() on the result of get_post_meta( $remote_id, $meta_key, true ) without checking that it is an array. Where the reciprocal relationship meta is stored as multiple single-value rows (one row per connected post ID) rather than one serialised array, get_post_meta( …, true ) returns the first row's value as a string, and array_diff() throws.
This is a hard fatal on wp_insert_post(), so the editor save fails and WordPress emails the site admin its "Your Site is Experiencing a Technical Issue" recovery-mode notice.
Error
PHP Fatal error: Uncaught TypeError: array_diff(): Argument #1 ($array) must be of type array, string given
in includes/classes/legacy/class-admin.php:253
Stack trace:
#0 includes/classes/legacy/class-admin.php(253): array_diff()
#1 includes/classes/legacy/class-admin.php(216): lsx\legacy\Admin->remove_connected_id()
#2 wp-includes/class-wp-hook.php(341): lsx\legacy\Admin->cpt_relations()
… CMB2_Field->save_field() → CMB2->save_fields() → CMB2_Hookup->save_post()
→ wp_insert_post() → wp_update_post() → edit_post()
Steps to reproduce
- Have a post whose relationship meta key (e.g.
destination_to_accommodation) exists as several separate wp_postmeta rows, each holding a single ID. This is the storage shape produced by older versions and by importers.
- Edit a post connected to it and remove one of the connections.
- Save.
cpt_relations() → remove_connected_id() → fatal.
Root cause
Three defects in the same code path:
1. Missing array guard (the fatal), class-admin.php:249-255
public function remove_connected_id( $remote_id, $connected_id, $meta_key ) {
$prev = get_post_meta( $remote_id, $meta_key, true );
if ( ! empty( $prev ) ) {
$diff = array_diff( $prev, array( $connected_id ) ); // $prev can be a string
update_post_meta( $remote_id, $meta_key, $diff, $prev );
}
}
The sibling add_connected_id() has an is_array() guard; the remove path does not. cpt_relations() normalises its own side with if ( ! is_array( $previous_values ) ) but never the remote side it writes to.
2. Silent data corruption via the $prev_value argument
Even once the fatal is guarded, update_post_meta( $remote_id, $meta_key, $diff, $prev ) is wrong for multi-row data. Per wp-includes/meta.php, a non-empty $prev_value adds meta_value = $prev_value to the WHERE clause, so only the row matching the old value is rewritten. On a key with N rows, one row is replaced with a serialised array and the other N-1 rows survive untouched — the disconnect half-applies and the key ends up in a mixed state.
Passing an empty $prev_value is not a fix either: update_metadata() then loops every $meta_id for that key and writes the same value to all of them, duplicating the array across N rows.
3. foreach over a scalar, class-admin.php:196-202
In the "all connections removed" branch, $previous_values is iterated without the is_array() normalisation that the else branch performs, warning on foreach() argument must be of type array|object and silently skipping the cleanup.
Impact
Any site with legacy multi-row relationship meta. On one affected install, over half of the ~20k relationship meta rows are in the scalar shape across destination_to_accommodation, destination_to_review, tour_to_accommodation, tour_to_destination and accommodation_to_destination — so most destination and tour saves that drop a connection hit it.
PHP 8 turned what used to be an array_diff(): Argument #1 must be of type array warning into a TypeError, which is why this surfaces as a fatal now rather than a silently failed save.
Proposed fix
Read and write relationship meta through a single normalising helper rather than assuming a shape:
- Read with
get_post_meta( $id, $key, false ) so every row is seen, flattening any row that is itself a serialised array.
- Normalise IDs to unique, non-empty, reindexed values.
- Write by deleting the key and adding one row holding a single serialised array, so a mixed key self-heals to the canonical shape on the next save.
- Guard the scalar
foreach in cpt_relations().
Affects main and every branch that touches this file (#682, #810, #1148) — all carry the identical unguarded call.
Summary
lsx\legacy\Admin::remove_connected_id()callsarray_diff()on the result ofget_post_meta( $remote_id, $meta_key, true )without checking that it is an array. Where the reciprocal relationship meta is stored as multiple single-value rows (one row per connected post ID) rather than one serialised array,get_post_meta( …, true )returns the first row's value as a string, andarray_diff()throws.This is a hard fatal on
wp_insert_post(), so the editor save fails and WordPress emails the site admin its "Your Site is Experiencing a Technical Issue" recovery-mode notice.Error
Steps to reproduce
destination_to_accommodation) exists as several separatewp_postmetarows, each holding a single ID. This is the storage shape produced by older versions and by importers.cpt_relations()→remove_connected_id()→ fatal.Root cause
Three defects in the same code path:
1. Missing array guard (the fatal),
class-admin.php:249-255The sibling
add_connected_id()has anis_array()guard; the remove path does not.cpt_relations()normalises its own side withif ( ! is_array( $previous_values ) )but never the remote side it writes to.2. Silent data corruption via the
$prev_valueargumentEven once the fatal is guarded,
update_post_meta( $remote_id, $meta_key, $diff, $prev )is wrong for multi-row data. Perwp-includes/meta.php, a non-empty$prev_valueaddsmeta_value = $prev_valueto theWHEREclause, so only the row matching the old value is rewritten. On a key with N rows, one row is replaced with a serialised array and the other N-1 rows survive untouched — the disconnect half-applies and the key ends up in a mixed state.Passing an empty
$prev_valueis not a fix either:update_metadata()then loops every$meta_idfor that key and writes the same value to all of them, duplicating the array across N rows.3.
foreachover a scalar,class-admin.php:196-202In the "all connections removed" branch,
$previous_valuesis iterated without theis_array()normalisation that theelsebranch performs, warning onforeach() argument must be of type array|objectand silently skipping the cleanup.Impact
Any site with legacy multi-row relationship meta. On one affected install, over half of the ~20k relationship meta rows are in the scalar shape across
destination_to_accommodation,destination_to_review,tour_to_accommodation,tour_to_destinationandaccommodation_to_destination— so most destination and tour saves that drop a connection hit it.PHP 8 turned what used to be an
array_diff(): Argument #1 must be of type arraywarning into aTypeError, which is why this surfaces as a fatal now rather than a silently failed save.Proposed fix
Read and write relationship meta through a single normalising helper rather than assuming a shape:
get_post_meta( $id, $key, false )so every row is seen, flattening any row that is itself a serialised array.foreachincpt_relations().Affects
mainand every branch that touches this file (#682, #810, #1148) — all carry the identical unguarded call.