Summary
The co-author list is currently stored as a single serialized array under the _map_co_authors postmeta key, and mutated with a read-modify-write pattern in Co_Authors::add_co_author() / ::remove_co_author(). Two concurrent writes (likely for the invite flow — "post goes viral, N users click at once") read the same snapshot, each append their own user, and last-write-wins drops the other additions silently.
Where
includes/class-map-co-authors.php, lines 68–104. Both add_co_author and remove_co_author fetch the current array via get_co_authors() and overwrite with update_post_meta().
Suggested fix — one row per co-author
Move to add_post_meta( $post_id, '_map_co_author', $user_id, false ) with unique = false, and query via get_post_meta( $post_id, '_map_co_author' ) which returns an array of values.
Benefits:
- Each add is an atomic
INSERT (after an existence check); no RMW window.
delete_post_meta( $post_id, '_map_co_author', $user_id ) is an atomic targeted delete.
is_co_author can be a single narrow query against the meta table.
- Same
wp_postmeta table, no schema changes.
The only concurrent-insert race remaining is two simultaneous adds for the same user, which is benign (idempotent check-then-insert — worst case, we get two identical rows; dedupe on read, or add a transient lock on the check).
Meta key change
Rename the meta key from _map_co_authors (array) to _map_co_author (singular, multiple rows). This makes them different keys so we don't trip over a stale serialized array if the plugin is downgraded, and makes the new shape self-describing.
Migration
On upgrade, one-shot migration:
add_action( 'init', __NAMESPACE__ . '\\maybe_migrate_co_authors_storage' );
function maybe_migrate_co_authors_storage(): void {
if ( '2' === get_option( 'map_db_version' ) ) {
return;
}
// Query all posts with the legacy meta, insert per-row, delete legacy.
// Batch via get_posts/paged. Guard with a transient/lock for safety.
update_option( 'map_db_version', '2' );
}
Or, if willing to accept one-time data loss (plugin has no releases yet), just switch the key and document it. Given there are no release users yet, the latter is fine for this PR.
Interaction with #8
The register_post_meta() call needs updating to the new key. Tightening the auth_callback (issue #8) happens in the same block, so these two could land together in one PR if convenient — or #7 first, then #8 on top.
Tests to add / update
- All existing
Test_Co_Authors tests should pass against the new storage.
- New test: concurrent add simulation — call
add_co_author for two different users after each has called get_co_authors (simulate RMW) and assert both end up stored.
- New test:
remove_co_author removes exactly the one user, leaves others.
References
Internal security review, Apr 2026. Likely lands alongside #8.
Summary
The co-author list is currently stored as a single serialized array under the
_map_co_authorspostmeta key, and mutated with a read-modify-write pattern inCo_Authors::add_co_author()/::remove_co_author(). Two concurrent writes (likely for the invite flow — "post goes viral, N users click at once") read the same snapshot, each append their own user, and last-write-wins drops the other additions silently.Where
includes/class-map-co-authors.php, lines 68–104. Bothadd_co_authorandremove_co_authorfetch the current array viaget_co_authors()and overwrite withupdate_post_meta().Suggested fix — one row per co-author
Move to
add_post_meta( $post_id, '_map_co_author', $user_id, false )withunique = false, and query viaget_post_meta( $post_id, '_map_co_author' )which returns an array of values.Benefits:
INSERT(after an existence check); no RMW window.delete_post_meta( $post_id, '_map_co_author', $user_id )is an atomic targeted delete.is_co_authorcan be a single narrow query against the meta table.wp_postmetatable, no schema changes.The only concurrent-insert race remaining is two simultaneous adds for the same user, which is benign (idempotent check-then-insert — worst case, we get two identical rows; dedupe on read, or add a transient lock on the check).
Meta key change
Rename the meta key from
_map_co_authors(array) to_map_co_author(singular, multiple rows). This makes them different keys so we don't trip over a stale serialized array if the plugin is downgraded, and makes the new shape self-describing.Migration
On upgrade, one-shot migration:
Or, if willing to accept one-time data loss (plugin has no releases yet), just switch the key and document it. Given there are no release users yet, the latter is fine for this PR.
Interaction with #8
The
register_post_meta()call needs updating to the new key. Tightening theauth_callback(issue #8) happens in the same block, so these two could land together in one PR if convenient — or #7 first, then #8 on top.Tests to add / update
Test_Co_Authorstests should pass against the new storage.add_co_authorfor two different users after each has calledget_co_authors(simulate RMW) and assert both end up stored.remove_co_authorremoves exactly the one user, leaves others.References
Internal security review, Apr 2026. Likely lands alongside #8.