Skip to content

fix(ios): gate the media upload server on the site root too - #624

Open
jkmassel wants to merge 1 commit into
fix/register-core-media-upload-middlewarefrom
fix/media-server-site-root-guard
Open

fix(ios): gate the media upload server on the site root too#624
jkmassel wants to merge 1 commit into
fix/register-core-media-upload-middlewarefrom
fix/media-server-site-root-guard

Conversation

@jkmassel

@jkmassel jkmassel commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Stacked on #594. First of ten PRs splitting #621; each is a single commit on the one before.

What?

iOS starts the native media upload server without checking that siteApiRoot is usable. Android has checked it since it landed.

Why?

startUploadServer guards on authHeader.isEmpty alone, though the comment directly above it says the uploader "needs a site root and an auth header". An iOS host that configured an auth header but no siteApiRoot started a server whose every request failed at the URLSession layer, instead of falling back to the WebView upload path the way Android does.

The two platforms diverged here unnoticed because the check lives in EditorViewController, which is #if canImport(UIKit) — it does not exist on the macOS host, so nothing in it is reachable from the test suite.

How?

  • ios/Sources/GutenbergKit/Sources/Media/MediaServerCredentials.swift: new. areUsable(siteApiRoot:authHeader:) owns the check, outside the UIKit gate so it is testable.
  • ios/Sources/GutenbergKit/Sources/EditorViewController.swift: startUploadServer calls it.

siteApiRoot is a URL here where Android types it as a String, so isEmpty() has no direct equivalent — "addressable" is spelled as scheme and host both being present.

Testing Instructions

Five tests pin the predicate, including both arms of the site-root check.

  • swift test — host suite green
  • iOS Simulator xcodebuild (the host build compiles EditorViewController as empty, so the gated change needs a simulator build)
  • SwiftLint clean

@github-actions github-actions Bot added the [Type] Bug An existing feature does not function as intended label Sep 5, 2026
@jkmassel jkmassel added the iOS label Sep 5, 2026
@jkmassel jkmassel self-assigned this Sep 5, 2026
@wpmobilebot

wpmobilebot commented Sep 5, 2026

Copy link
Copy Markdown

XCFramework Build

This PR's XCFramework is available for testing. Add the following to your Package.swift:

.package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/624")

Built from dadd64b

@jkmassel
jkmassel requested a review from dcalhoun September 8, 2026 15:17
`startUploadServer` checked only `authHeader.isEmpty`, though the comment
directly above it said the uploader "needs a site root and an auth
header". Android has checked both since it landed. An iOS host that
configured an auth header but no `siteApiRoot` therefore started a server
whose every request failed at the URLSession layer, instead of falling
back to the WebView upload path the way Android does.

`siteApiRoot` is a `URL` here where Android types it as a `String`, so
`isEmpty()` has no direct equivalent — "addressable" is spelled as scheme
and host both being present.

Put the check in `MediaServerCredentials` rather than inline.
`EditorViewController` is `#if canImport(UIKit)`, so it does not exist on
the macOS host and nothing in it is reachable from the test suite — which
is how the two platforms diverged here unnoticed. Outside the gate, the
predicate gets five tests, including both arms of the site-root check.
@jkmassel
jkmassel force-pushed the fix/media-server-site-root-guard branch from 72b6329 to dadd64b Compare September 8, 2026 16:11
@jkmassel
jkmassel marked this pull request as ready for review September 8, 2026 16:29

@dcalhoun dcalhoun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changes look sound. I tested uploading an image through the native proxy.

@dcalhoun dcalhoun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Capturing a few findings by Claude that I overlooked earlier.

Comment on lines +474 to +477
guard MediaServerCredentials.areUsable(
siteApiRoot: configuration.siteApiRoot,
authHeader: configuration.authHeader
) else {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Finding from Claude:

This widens the gate from !authHeader.isEmpty to also require an addressable siteApiRoot — but a delegate that handles uploads itself never touches DefaultMediaUploader. MediaUploadServer.processAndUpload returns .uploaded(result) straight from uploadDelegate without resolving context.defaultUploader, so that host now loses its own upload path over a site root it never uses.

Intended? If not, the site-root arm belongs where defaultUploader is actually built.


@Test("rejects a site root with no scheme")
func rejectsSchemelessSiteRoot() {
let relative = URL(string: "example.com/wp-json/")!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Finding from Claude:

URL(string: "example.com/wp-json/") has no host either (scheme=nil, host=nil), so this passes on the host arm alone — and rejectsEmptySiteRoot already covers that shape. Nothing isolates scheme != nil; drop it from areUsable and all five tests stay green.

A network-path reference separates them (scheme=nil, host="example.com"):

Suggested change
let relative = URL(string: "example.com/wp-json/")!
let relative = URL(string: "//example.com/wp-json/")!

#expect(!MediaServerCredentials.areUsable(siteApiRoot: fileURL, authHeader: "Bearer t"))
}

@Test("rejects an empty site root, the default when a host configures none")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Finding from Claude:

siteApiRoot has no default — it's required on EditorConfiguration.init, on EditorConfigurationBuilder.init, and on Android's builder. A host can't configure none; "/" is a value someone passes deliberately.

Suggested change
@Test("rejects an empty site root, the default when a host configures none")
@Test("rejects a site root with no scheme or host")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

iOS [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants