Skip to content

Accept file URLs for support ticket attachments - #1623

Open
jkmassel wants to merge 1 commit into
trunkfrom
jkmassel/support-attachment-url-params
Open

Accept file URLs for support ticket attachments#1623
jkmassel wants to merge 1 commit into
trunkfrom
jkmassel/support-attachment-url-params

Conversation

@jkmassel

@jkmassel jkmassel commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Adds attachmentURLs: [URL] initializers to CreateSupportTicketParams and AddMessageToSupportConversationParams, so the Swift wrapper owns the URL → filesystem path conversion instead of leaving it to each caller.

Summary

  • Attachments cross the bindings as filesystem paths, and SafeRequestExecutor opens each one directly.
  • A caller holding a file URL has to convert it, and URL.path() — the obvious choice — percent-encodes by default. That hands the executor a path that doesn't exist on disk, and the request fails with MediaFileNotFound.
  • A filename with a space is enough. Non-ASCII names are escaped too.

Why this belongs in the library

The conversion isn't a caller preference — there is exactly one correct answer, and the library is the only place that knows the string is about to be opened rather than put in a URL. Leaving it to callers means every consumer gets its own chance to be wrong, and the failure is invisible until a user picks a file whose name happens to need encoding.

WordPress-iOS hit this in production on the media path (WordPress-iOS#26005) and then again on support attachments (WordPress-iOS#26008) — same defect, two call sites, found only by grepping for the shape after the first one. That's the argument for moving it down here.

The attachment filename is user data, not app-generated. On iOS it's whatever PhotosUI exports, which preserves the asset's original name — and macOS names screenshots Screen Shot 2026-09-08 at 10.31.15.png.

let url = URL(fileURLWithPath: "/tmp/Screen Shot 1.png")
url.path()                       // "/tmp/Screen%20Shot%201.png"  ❌
url.path(percentEncoded: false)  // "/tmp/Screen Shot 1.png"      ✅

Changes

native/swift/Sources/wordpress-api/WPComExtensions.swift: two convenience initializers taking attachmentURLs: [URL], mirroring the generated memberwise parameter lists so no field is lost.

attachmentURLs deliberately has no default value. Giving it one would make a call passing no attachments match both this initializer and the generated one.

The existing attachments: [String] initializers are unchanged — a caller that already has paths keeps working, and this is additive.

Test plan

  • Added SupportTicketAttachmentTests — seven cases across both params types: a filename with a space, one with a %, one non-ASCII, controls needing no encoding, an empty attachment list, and a pass-through check that every other field survives the convenience initializer.
  • Added two cases to SupportTicketsCompatTests so the api-compatibility suite pins the new signatures.
  • swift test --filter SupportTicketAttachmentTests — 7/7 pass.
  • make lint-swift clean.

Notes

Two things I did not do here, both worth a second opinion:

MediaCreateParams has the same shape

It's the third RequiresMultipartForm type and the one that caused the original production bug. It's a wp_api type rather than a WP.com one, so it'd land in a different file, and WordPress-iOS#26005 is currently fixing it caller-side. Happy to add it here instead if you'd rather have all of them consistent. (ReplyToUnifiedConversationParams is the fourth; no consumer in WordPress-iOS today.)

The parameter lists can drift

A convenience initializer that forwards to a generated memberwise initializer has to be updated whenever the Rust record gains a field, and nothing catches it — the new field just isn't reachable through the URL-based initializer. There's a comment saying so, but if you'd rather avoid the maintenance entirely, the drift-free alternative is a named conversion on URL that every params type can use:

public extension URL {
    var multipartFilePath: String { path(percentEncoded: false) }
}

That covers all four types at once and never needs syncing, but it doesn't stop a caller from reaching for path() anyway. I went with the initializers because preventing the mistake seemed worth more than the upkeep — say the word if you disagree.

Attachments cross the bindings as filesystem paths and the request executor
opens each one directly, so a caller holding a file URL has to convert it.
URL.path() percent-encodes by default, which produces a path that doesn't
exist on disk and fails the request with MediaFileNotFound — a filename with
a space is enough, and non-ASCII names are escaped too.

Adds attachmentURLs: [URL] initializers to CreateSupportTicketParams and
AddMessageToSupportConversationParams so the wrapper owns the conversion.
The existing attachments: [String] initializers are unchanged.

WordPress-iOS hit this on the media path and then again on support
attachments; moving the conversion here means each consumer doesn't get its
own chance to be wrong.
@wpmobilebot

Copy link
Copy Markdown
Collaborator

XCFramework Build

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

.package(url: "https://github.com/automattic/wordpress-rs", branch: "pr-build/1623")

Built from c75ae8b

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.

2 participants