Skip to content

Only seed the "Powered by" profile field once - #3666

Open
pfefferle wants to merge 8 commits into
trunkfrom
fix/powered-by-flag
Open

Only seed the "Powered by" profile field once#3666
pfefferle wants to merge 8 commits into
trunkfrom
fix/powered-by-flag

Conversation

@pfefferle

@pfefferle pfefferle commented Aug 25, 2026

Copy link
Copy Markdown
Member

Fixes #3280
Fixes #3339

Proposed changes:

Two bugs, one missing flag.

Extra_Fields::default_actor_extra_fields() writes an activitypub_default_extra_fields flag when it provisions defaults, and checks that flag before provisioning again. Migration::add_default_extra_field(), the one that adds the "Powered by" entry, never wrote it. So:

  • Deleting the entry left the actor with an empty field list and no flag, and the next get_actor_fields() call spawned Blog/Profile/Homepage replacements. That is what people see as the field "reappearing" (**Bug report: "Powered by WordPress" extra field duplicates randomly on post edits** #3280).
  • Nothing recorded that the seeding had happened. activitypub_db_version is only written at the very end of maybe_migrate(), so a migration that fails or times out before that replays the whole initial migration and adds another "Powered by" entry. The lock clears itself after 30 minutes, so it can repeat (Powered by WordPress #3339).

The fix is to claim the actor with that flag before inserting, add_user_meta( ..., $unique = true ) for users and add_option() for the blog actor. Both only write when the row does not exist yet, so a replay is a no-op and a field somebody deleted is not seeded again.

backfill_default_extra_fields_flag() sets the flag on existing installs. It only marks actors that still hold an extra field, because those were provisioned by an earlier run. Actors with no fields are left alone, they may have joined after the initial migration and are still owed their defaults.

The flag is stored as a user option now, so it is scoped per site on multisite, which matches extra fields being per-site posts. get_user_option() falls back to the unprefixed key, so flags written before this keep working and nobody gets re-provisioned. There is no add_user_option(), so the seeder adds the site prefix by hand to keep the atomic claim.

There is no cleanup routine for duplicates that already exist. Matching them by title would mean force-deleting user-editable content on init, and they can be removed in Settings, ActivityPub. Existing duplicates stop multiplying either way.

Other information:

  • Have you written new tests for your changes, if applicable?

Supersedes #3638 by @faisalahammad, which fixed the duplication with a per-user title lookup plus a cleanup migration.

Testing instructions:

  • Fresh install. Confirm exactly one "Powered by" field per actor in Settings, ActivityPub.
  • Delete it, then reload the settings screen or fetch the actor. It must stay gone, and no Blog/Profile/Homepage fields should appear in its place.
  • Simulate a replayed migration with wp option delete activitypub_db_version and load any page. No second "Powered by" field should show up.
  • On a site that upgrades, confirm actors that already have extra fields get the activitypub_default_extra_fields flag, and that a user created after the upgrade still gets Blog/Profile/Homepage defaults on first fetch.

Changelog entry

  • Automatically create a changelog entry from the details below.

A changelog entry was added manually as .github/changelog/fix-powered-by-flag.

Changelog Entry Details

Significance

  • Patch
  • Minor
  • Major

Type

  • Added - for new features
  • Changed - for changes in existing functionality
  • Deprecated - for soon-to-be removed features
  • Removed - for now removed features
  • Fixed - for any bug fixes
  • Security - in case of vulnerabilities

Message

The "Powered by WordPress" profile field is now only added once, and stays gone when you delete it.

The 'Powered by WordPress' field re-appearing after deletion (issue #3280)
isn't a duplicate of the same field — it is Extra_Fields::default_actor_extra_fields
spawning Blog/Profile/Homepage replacements once the user's list goes empty.

Migration::add_default_extra_field created the Powered-by entry on initial install
but never set the activitypub_default_extra_fields flag, so the filter's
$already_migrated check never saw it. Deleting the Migration-provisioned entry left
the user in a state where the next get_actor_fields() call (during federation,
admin UI loads, or Mastodon API responses) triggered the filter, which created
a fresh set of unrelated defaults.

Setting the flag in add_default_extra_field stops the spawn on new installs.
A version-gated backfill (backfill_default_extra_fields_flag) does the same
for sites that already went through the initial migration.

Fixes #3280
Copilot AI lite review requested due to automatic review settings August 25, 2026 14:44
@pfefferle pfefferle added the Bug Something isn't working label Aug 25, 2026
@pfefferle pfefferle self-assigned this Aug 25, 2026
@pfefferle
pfefferle requested a review from a team August 25, 2026 14:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes repeated seeding of the default “Powered by WordPress” extra field by introducing an explicit provisioning flag (activitypub_default_extra_fields) that is claimed atomically before inserts, and by backfilling that flag for existing installs during migration.

Changes:

  • Make Migration::add_default_extra_field() claim the provisioning flag before inserting, so replayed migrations become no-ops and deletions don’t trigger other defaults.
  • Add a new migration step backfill_default_extra_fields_flag() to set the flag for actors that already have extra fields.
  • Add PHPUnit coverage for “delete stays deleted”, replayed migrations, and backfill behavior; add two changelog entries.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
includes/class-migration.php Claims activitypub_default_extra_fields before seeding “Powered by”, and adds a backfill migration for the flag.
tests/phpunit/tests/includes/class-test-migration.php Adds regression tests ensuring deletion doesn’t re-provision defaults and replays don’t reseed.
.github/changelog/fix-powered-by-flag Changelog entry for deletion “sticking” instead of being replaced by other defaults.
.github/changelog/fix-powered-by-duplicates Changelog entry for preventing repeated “Powered by WordPress” insertions.
Suppressed comments (1)

includes/class-migration.php:823

  • For the blog actor, the option flag is claimed with add_option() and then the post is inserted, but the insert result isn't checked. If the insert fails after the option is set, the blog actor becomes permanently marked as provisioned and later runs won't be able to retry seeding.
					'post_content' => $content,
				)
			);
		}

		// add_option() only writes when the row does not exist yet, which claims the blog actor the same way.
		if ( \add_option( 'activitypub_default_extra_fields', true, '', false ) ) {
			\wp_insert_post(
				array(
					'post_type'    => Extra_Fields::BLOG_POST_TYPE,
					'post_author'  => 0,
					'post_status'  => 'publish',

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread includes/class-migration.php
Comment thread includes/class-migration.php
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Powered by WordPress **Bug report: "Powered by WordPress" extra field duplicates randomly on post edits**

2 participants