Automattic / Automattic/wordpress-activitypub
Feature Request: Add C2S media upload support (uploadMedia endpoint)
- Dominant language
- PHP
- Stars
- 579
- Forks
- 92
- Avg merge
- 13h 5m
- Merged PRs (30d)
- 85
Description
## What
Add ActivityPub Client-to-Server (C2S) media upload support, so OAuth-authenticated clients can push binaries (images, audio, video, generic documents) into the WordPress media library and receive back an ActivityPub object they can then reference in a subsequent outbox `Create`.
## Why
The plugin already implements C2S for JSON activities through the OAuth-protected outbox, but there is no way to upload a binary. Any third-party AP client that wants to post media has to fall back to the Mastodon REST API or to a WordPress-specific upload route, which defeats the purpose of speaking ActivityPub C2S in the first place.
The two specs the user asked about:
- W3C SocialCG Wiki: https://www.w3.org/wiki/SocialCG/ActivityPub/MediaUpload
- SWICG `activitypub-api`: https://github.com/swicg/activitypub-api (and issue [#6](https://github.com/swicg/activitypub-api/issues/6))
Tracking discussion: [w3c/activitypub#445](https://github.com/w3c/activitypub/issues/445) ("Finish the uploadMedia definition", still open as of 2026).
## Current state in the plugin
| Capability | Status | Reference |
|---|---|---|
| Outbox accepts JSON activities (C2S) | yes | `includes/rest/class-outbox-controller.php` (`get_json_params()` only) |
| Outbox accepts `multipart/form-data` | no | same controller, no multipart parser |
| Actor advertises `endpoints.uploadMedia` | no | `class-user.php`, `class-blog.php` (only `sharedInbox`, OAuth endpoints, `proxyUrl`, `proxyEventStream`) |
| OAuth scope for media (`upload`, `media:write` etc.) | no | `includes/oauth/class-scope.php` (only `read`, `write`, `follow`, `push`, `profile`) |
| Outbound `Image`/`Audio`/`Video` attachments | yes | `includes/transformer/class-base.php` (FEP-ee3a friendly) |
| Inbound media handling on `Create`/`Update` | no | no handler accepts a `file` part |
| Tests for media upload | none | `tests/phpunit/tests/includes/rest/class-test-outbox-controller.php` is JSON-only |
## What the spec(s) say
**Wiki shape (2017, never made it into the REC):**
- Actor advertises a dedicated endpoint via `endpoints.uploadMedia`.
- `multipart/form-data` POST with two parts: `object` (an AS shell, e.g. a `Create` wrapping a `Video`) plus `file` (the binary).
- Explicitly **not** the outbox: *using this endpoint does not involve submitting to the outbox.*
- Response: `201 Created` or `202 Accepted` with a `Location` header pointing at the new object `id`. Server populates `url` with one or more `Link` objects, handy for transcodes/thumbnails with `width`, `height`, `mediaType`.
**SWICG `activitypub-api`:** spec file is a 6-line stub. Issue #6 is a placeholder.
**Compat reality check:**
- Mastodon: doesn't implement AP C2S at all. Uses its own `/api/v2/media`.
- Pleroma: ships a variant. File only, returns `201` with the bare object body (no `Location`, no `id`). The client embeds the returned object as an `attachment` in a follow-up outbox `Create`. Namespaced under `https://www.w3.org/ns/activitystreams#uploadMedia`, which is technically squatting on the AS namespace.
- onepage.pub and the `ap` CLI (Evan Prodromou): wiki-style.
## How (proposed)
Wiki-flavored primary, with Pleroma-style fallback for interop:
1. **New REST route** `POST /activitypub/1.0/actors/{user_id}/uploadMedia`, `multipart/form-data` only, OAuth bearer required.
2. **New scope** `upload` (or document that `write` covers it, TBD during implementation).
3. **Advertise it** in `Actor::get_endpoints()` on `User` and `Blog`. `Application` is system-only, skip.
4. **Handler**
- Parse the `file` part via `wp_handle_upload()` so WordPress MIME validation, the `upload_mimes` filter, and quota apply for free.
- Parse the `object` part as JSON-LD. If absent, fall back to Pleroma-style (file only, build a minimal `Image`/`Video`/`Audio` shell from the MIME).
- Insert as a WP attachment (`wp_insert_attachment`) so it lives in the media library with its own permalink.
- Build the AP object via the existing `Base::get_attachment()` path. That already produces FEP-ee3a-friendly output with dimensions and EXIF, so we should not duplicate it.
- Per spec, do **not** auto-publish even if the inbound `object` was wrapped in a `Create`. The client decides what to do next.
- Return `201 Created` with `Location: ` and the AP object body. For Pleroma compat the body itself is enough.
5. **Tests** along the lines of `class-test-outbox-controller.php`: multipart fixture, OAuth bearer, asserts on `Location`, asserts attachment was inserted, asserts it's reachable via its AP `id`.
6. **FEDERATION.md** row under C2S noting wiki-flavored `uploadMedia` with Pleroma-fallback semantics.
## Open questions
- The wiki spec is silent on max size, allowed MIME types, chunked/resumable upload, error formats, cancellation, progress.
- Two incompatible flavors in the wild: implementing only one will break interop with the other. Mitigation: accept either shape on the server side.
- The wiki spec is ambiguous on whether the server "MAY wrap" in a `Create`, so clients can't reliably predict whether `Location` points to the `Create` or the bare media object. Proposal: always return the bare object, never auto-wrap, so behavior is predictable.
- Should this be gated behind a "Beta features" setting until SWICG actually finishes the spec?
- Does this need its own OAuth scope (`upload`) or fold into `write`?
## Out of scope
- The Mastodon REST API media endpoint. That is already handled by the separate Mastodon API bridge work and has nothing to do with AP C2S.
- Chunked / resumable upload. Add later if/when the spec defines it.
Contributor guide
Assessment
This issue has not been assessed yet.