Skip to content

When update-core.php version scraping goes wrong — telling a plugin version number apart from WordPress core

On hosting without SSH access, a common pattern is to open update-core.php (the WordPress update screen) with Playwright and read “what version is WordPress core currently running” straight off the page text. In one deployment, the recorded core version came back as something like 1.7.11 — a number that has never existed for WordPress core.

Note: update-core.php is the WordPress admin’s “Updates” page, listing pending updates for core, plugins, themes, and translations all on one screen.

What was actually happening

That page doesn’t only show the core version string — it’s packed with version numbers belonging to pending plugin and translation updates too. A line like “Update Plugin X to 1.7.11” is typical.

# First implementation — grabs the first N.N.N-shaped number on the page
match = re.search(r'\d+\.\d+(?:\.\d+)?', page_text)
version = match.group(0) if match else None

This naive regex grabs whatever N.N.N-shaped number appears first on the page. Because of how the page is laid out, a plugin’s pending update can render above the core version message, so 1.7.11 (a plugin’s version) ended up recorded as the WordPress core version.

Why this is hard to catch

The bug doesn’t throw an exception — the regex matches successfully, just on the wrong value. Nothing about it looks broken until someone notices the report shows a version that WordPress core has never shipped (there’s no 1.x series for core).

The fix — a three-stage guard

  1. Prefer a dedicated selector first — look for specific DOM locations where core’s update message actually renders, like p.response > strong or #wp-version-message strong
  2. Fall back to keyword-anchored regex — if no selector matches, only accept a number immediately following the words “WordPress,” “バージョン,” or “Version”
  3. Validate plausibility as a final check — whichever path produced a value, run it through a function that checks the major version number falls within 4–9
def is_plausible_wp_core_version(ver: str) -> bool:
    m = re.match(r'^(\d+)\.(\d+)(?:\.(\d+))?$', ver.strip())
    if not m:
        return False
    major = int(m.group(1))
    return 4 <= major <= 9

WordPress core’s major version currently sits in the 4–9 range. Plugin version numbers tend to cluster in 1.x–3.x, so this single range check filters out most false positives on its own.

What makes this design work

This isn’t just “tighten the regex.” The real structure is two layers of defense: rank extraction sources by trustworthiness (DOM selector first), then validate the meaning of whatever value comes out, regardless of which path produced it. Scraped text matching a regex pattern doesn’t guarantee the match is semantically correct — that assumption gets baked directly into the code’s structure.

Summary

Stage What it does
1. Selector first Look for the specific DOM location where core’s update message renders
2. Keyword-anchored fallback Only accept numbers directly following “WordPress” / “Version”
3. Plausibility check Reject anything outside the major-version-4–9 range as a final defense

Like Playwright strict-mode violations colliding with wp-admin’s repeated selectors or the recurring “confirm your admin email” screen stalling automation, this starts from the same premise: wp-admin’s screens can’t be taken at face value. This time it’s not about what gets clicked but what a scraped value actually means — and the habit of validating scraped text before trusting it is what prevents this kind of silent corruption. The same “looked successful but wasn’t” gap also shows up in select-all checkboxes that don’t actually select anything.