Skip to content

Make wpApiRestUrl, xmlRpcUrl, and app-password creds single-writer#22947

Draft
jkmassel wants to merge 6 commits into
trunkfrom
jkmassel/sitesqlutils-partial-updates
Draft

Make wpApiRestUrl, xmlRpcUrl, and app-password creds single-writer#22947
jkmassel wants to merge 6 commits into
trunkfrom
jkmassel/sitesqlutils-partial-updates

Conversation

@jkmassel
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel commented Jun 5, 2026

Description

Fixes #22905. Several SiteModel columns are discovered or healed out of band — not carried by the general site-sync responses — yet every full-row insertOrUpdateSite UPDATE wrote them. When the in-memory SiteModel came from a source that doesn't carry a field (/me/sites, /sites/<id>, the RN bridge, cookie-nonce auth), the full-row write stamped its stale/null over a good value. On Atomic sites the recovered wpApiRestUrl was wiped on every app foreground; the same mechanism zeroes the application-password credential columns and xmlRpcUrl.

Root cause

SiteSqlUtils.insertOrUpdateSite updates via WellSql.update().put(model, UpdateAllExceptId(...)), which writes every column except _id. Per-handler preservation existed for some columns but was gated on encrypted AP creds being present — which Atomic sites don't have — so it didn't fire for them, and it was TOCTOU-fragile regardless.

Fix

Make targeted single-column writers authoritative and exclude these columns from the generic mapper. UpdateAllExceptId now accepts additional columns to skip; insertOrUpdateSite's UPDATE excludes them. INSERT is untouched, so new sites still persist everything.

Three column families, each with a targeted writer as the sole writer on an existing row:

1. WP_API_REST_URL

  • updateWpApiRestUrl (the heal writer from Populate missing wpApiRestUrl during editor preload #22903) becomes the sole writer; clearWpApiRestUrl for sign-out.
  • Rerouted the discover/persist callers that previously relied on full-row writes: updateApplicationPassword, CookieNonceAuthenticator, ReactNativeStore, and fetchSiteWPAPIFromApplicationPassword (URL-keyed, since the freshly fetched site has no local id).

2. Application-password credentials

  • API_REST_USERNAME, API_REST_PASSWORD and their two IV columns, excluded as a set — the IVs are required to decrypt the ciphertext, so protecting the values without them would break reads.
  • New updateApplicationPasswordCredentials (encrypts) / clearApplicationPasswordCredentials. Wired into updateApplicationPassword, removeApplicationPassword (full sign-out — also clears wpApiRestUrl), clearApplicationPasswordColumns (credential rotation — intentionally preserves wpApiRestUrl), and the WPAPI app-password fetch.

3. XMLRPC_URL

Out of scope / follow-ups

  • The now-redundant gated wpApiRestUrl preservation blocks in updateSite / createOrUpdateSites are left in place — harmless, optional cleanup.
  • Unify site provisioning + capability detection into one per-site source #22944 rebases on this PR: it drops its duplicate updateXmlRpcUrl (provided here) and keeps the SiteXmlRpcUrlRecoverer that consumes it.
  • Pre-existing :libs:fluxc:lintDebug noise (mostly DoNotMockDataClass) is unrelated and untouched. Detekt is clean.

Testing instructions

Automated

  1. Run ./gradlew :libs:fluxc:testDebugUnitTest --tests "*SiteSqlUtilsTest" --tests "*SiteStoreTest"
  • All pass. SiteSqlUtilsTest includes a clobber-prevention guard per family (a stale full-row update preserves the column while still updating other columns), plus the targeted writers.

Manual — Atomic site (the original repro)

  1. Sign in with an Atomic site (e.g. *.jurassic.ninja) and open it so wpApiRestUrl is discovered/healed.
  2. Manually NULL the WP_API_REST_URL column for that site row.
  3. Kill and relaunch, then background/foreground a few times (triggers FETCH_SITE/FETCH_SITES).
  • WP_API_REST_URL is healed and then survives — no longer reset to NULL.

Manual — self-hosted application-password site

  1. Add an application password to a self-hosted site, then load My Site a few times.
  • Credentials persist; the connectivity probe doesn't surface a false "Unable to connect to your site" banner.
  1. Remove the application password.
  • Credentials and wpApiRestUrl are cleared.

On Atomic / Jetpack-WPCom-REST sites a recovered wpApiRestUrl was wiped
to NULL on every app foreground: any full-row insertOrUpdateSite UPDATE
built from a partial in-memory SiteModel (FETCH_SITE/FETCH_SITES, RN,
cookie-nonce) wrote every column, clobbering the healed value.

Exclude WP_API_REST_URL from the generic UpdateAllExceptId mapper so
updateWpApiRestUrl is the sole writer on an existing row, and route every
legitimate writer through the targeted helpers.

Fixes #22905.
@dangermattic
Copy link
Copy Markdown
Collaborator

1 Message
📖 This PR is still a Draft: some checks will be skipped.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Jun 5, 2026

App Icon📲 You can test the changes from this Pull Request in WordPress Android by scanning the QR code below to install the corresponding build.

App NameWordPress Android
Build TypeDebug
Versionpr22947-b45ba1e
Build Number1493
Application IDorg.wordpress.android.prealpha
Commitb45ba1e
Installation URL2ktusom2jmbg8
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Jun 5, 2026

App Icon📲 You can test the changes from this Pull Request in Jetpack Android by scanning the QR code below to install the corresponding build.

App NameJetpack Android
Build TypeDebug
Versionpr22947-b45ba1e
Build Number1493
Application IDcom.jetpack.android.prealpha
Commitb45ba1e
Installation URL61lug8s850478
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot
Copy link
Copy Markdown
Contributor

wpmobilebot commented Jun 5, 2026

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

jkmassel added 2 commits June 5, 2026 13:19
The same full-row insertOrUpdateSite UPDATE that clobbered wpApiRestUrl
also zeroed the application-password credential columns: a credential-less
inbound SiteModel (e.g. a /me/sites sync) overwrote the encrypted
username/password and their IVs with empty values.

Exclude API_REST_USERNAME, API_REST_PASSWORD and their IV columns from the
UpdateAllExceptId mapper (as a set — the IVs are required to decrypt), and
route the credential writers through targeted single-column writers.

Extends the #22905 fix to the credential columns.
xmlRpcUrl is the same kind of out-of-band-discovered column as
wpApiRestUrl: a full-row insertOrUpdateSite UPDATE from a partial
SiteModel (e.g. a /sites sync whose response omits meta.links) writes a
stale/null over a good value.

Exclude XMLRPC_URL from the UpdateAllExceptId mapper and add a targeted
updateXmlRpcUrl writer as its sole writer on an existing row. The
xmlRpcUrl recovery flow that calls it lands separately.

Extends the #22905 fix to XMLRPC_URL.
@jkmassel jkmassel changed the title Stop full-row site writes from clobbering wpApiRestUrl Make wpApiRestUrl, xmlRpcUrl, and app-password creds single-writer Jun 5, 2026
attemptXmlRpcRediscovery dispatched newUpdateSiteAction to change one
field, which (with XMLRPC_URL excluded from the full-row mapper) dropped
the value and rewrote ~80 unchanged columns. Add SiteStore.persistXmlRpcUrl
and route rediscovery through it — one targeted write, mirroring the
wpApiRestUrl heal. Drops the now-unused dispatcher from the slice.
eq(xmlRpcUrl), any(), any()
)
).thenReturn(SitesModel(listOf(SiteModel())))
whenever(siteStore.persistXmlRpcUrl(any(), any())).thenReturn(mock())
jkmassel added 2 commits June 5, 2026 15:03
createOrUpdateSites (the XML-RPC app-password login store path) does a
full-row write that excludes the credential columns, with no targeted
writer to follow up — so re-logging into an already-stored self-hosted
site silently dropped the credentials.

Add SiteSqlUtils.insertOrUpdateSiteReturningId, which returns the local
id of the row it wrote; insertOrUpdateSite becomes a thin rows-affected
wrapper over it (behavior unchanged). createOrUpdateSites uses the
returned id to persist credentials + wpApiRestUrl via the targeted
writers on the exact row.
- The cookie-nonce and React Native 404 handlers reset wpApiRestUrl in
  memory to force rediscovery on retry, then persisted via
  insertOrUpdateSite/persistSiteSafely — now a no-op for the excluded
  column. Drop the dead DB write (keep the in-memory reset), which also
  orphaned ReactNativeStore.persistSiteSafely and its injected persist
  function; remove those.
- updateSite / createOrUpdateSites copied credentials + wpApiRestUrl from
  the DB onto the inbound model before the write; the mapper exclusion
  already preserves those columns, so drop the moot copy (editor-prefs
  copy stays).
- Rework ReactNativeStoreWPAPITest to mock SiteSqlUtils and assert on
  updateWpApiRestUrl (the discover path persists there now).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Recovered wpApiRestUrl doesn't survive across launches on Atomic sites

4 participants