Opened 10 years ago
Last modified 2 days ago
#40365 assigned enhancement
Introduce a REST API endpoint for sites
| Reported by: | jeremyfelt | Owned by: | realloc |
|---|---|---|---|
| Priority: | normal | Milestone: | Future Release |
| Component: | REST API | Version: | |
| Severity: | normal | Keywords: | dev-feedback ms-roadmap has-patch has-unit-tests changes-requested early |
| Cc: | Focuses: | multisite, rest-api |
Description
It should be possible to manage sites in a multisite configuration through the REST API.
- List sites:
GET wp/v2/sites/ - Retrieve a site:
GET wp/v2/sites/<id> - Create a site:
POST wp/v2/sites/ - Update a site:
PUT wp/v2/sites/<id> - Delete a site:
DELETE wp/v2/sites/<id>
Data included in a site object should at least mirror the data available for the site in wp_blogs. Additional ideal pieces of data for a site include blogname, blogdescription, home, and siteurl. It's possible that creating a new meta table for sites can help developers register meta for inclusion with a site object (See #37923).
Sites should be accessible by default for authenticated users only. Network (global) admins should have access to all sites. Site users should have access to the sites they are members of. The "My Sites" list is a great candidate for exploring how this will work. See #15317.
As of the introduction of get_sites() in 4.6.0, retrieving sites is a much better experience. The methods used to create, update, and delete sites in multisite are not as pleasant right now. We should investigate each of these and determine what can be done to streamline the process. The first improvement is probably in creating a site. See #40364.
Attachments (2)
Change History (46)
This ticket was mentioned in Slack in #core-multisite by jeremyfelt. View the logs.
10 years ago
This ticket was mentioned in Slack in #core-multisite by jeremyfelt. View the logs.
9 years ago
This ticket was mentioned in Slack in #core-restapi by flixos90. View the logs.
9 years ago
This ticket was mentioned in Slack in #core-multisite by jeremyfelt. View the logs.
9 years ago
This ticket was mentioned in Slack in #core-restapi by jnylen. View the logs.
9 years ago
#7
@
9 years ago
- Component Networks and Sites → REST API
- Keywords needs-patch, dev-feedback, needs-unit-tests, ms-roadmap → needs-patch dev-feedback needs-unit-tests ms-roadmap
This ticket was mentioned in Slack in #core-multisite by flixos90. View the logs.
9 years ago
#10
@
9 years ago
- Keywords has-patch added; needs-patch removed
First pass at patch. 40365.diff
It uses wpmu_create_blog, wpmu_delete_blog and update_blog_details for now, as other crud functions are not available.
This ticket was mentioned in Slack in #core-restapi by laygen. View the logs.
9 years ago
#12
@
9 years ago
This looks like a good start! I wonder if we could perhaps add this to a github PR on the existing repo) and do feedback there, as doing so on a patch this size is really no fund!
https://github.com/WP-API/wp-api-site-endpoints is the repo, on the PR it might be to write down why / if this is different than what exists there already.
This ticket was mentioned in Slack in #core-multisite by flixos90. View the logs.
8 years ago
This ticket was mentioned in Slack in #core-multisite by flixos90. View the logs.
8 years ago
This ticket was mentioned in Slack in #core-multisite by aslandro. View the logs.
19 months ago
#17
@
19 months ago
Thank You @jeremyfelt for opening this ticket. but I have a question!
Would this also include the ability to manage site options through the API, or would that be handled separately? Also, would there be filters/hooks to extend the response structure?
This ticket was mentioned in Slack in #core by realloc. View the logs.
13 months ago
This ticket was mentioned in Slack in #core by desrosj. View the logs.
13 months ago
#20
@
2 months ago
- Owner set to
- Status new → assigned
A sites endpoint is a prerequisite for #63885, the DataViews rewrite of the network admin site list. Multisite has no REST coverage for sites today.
The work was originally done in the feature plugin at https://github.com/WP-API/wp-api-sites-endpoints, where each fix is a separate commit with its own regression test.
The following PR ports that plugin into core. It had been sitting untouched for years while multisite kept moving, so some bugs were expected. I fixed them along the way, which resulted in a substantial PR: https://github.com/WP-API/wp-api-sites-endpoints/pull/5
This ticket was mentioned in PR #13055 on WordPress/wordpress-develop by @realloc.
8 weeks ago
#21
- Keywords has-unit-tests added; needs-unit-tests removed
Trac ticket: https://core.trac.wordpress.org/ticket/40365
There are some open questions that we should discuss in the trac ticket.
#22
@
7 weeks ago
- Milestone Future Release → 7.2
Moved to the 7.2 milestone. Landing native Multisite REST API endpoints in Core is long overdue, particularly as WordPress accelerates its reliance on modern admin architectures, programmatic orchestration, and AI-driven tooling.
Why Native Multisite Endpoints Are Critical
Core Modernization & Data Views: Core’s administrative UI—from the Site Editor to the rollout of Data Views—now relies entirely on the REST API. As Data Views expand to replace legacy admin tables across the network dashboard, native endpoints are mandatory to render, filter, and modify network-level data. Multisite currently stands out as the only major Core feature with 0% API coverage.
AI Tooling & MCP Connectors: Model Context Protocol (MCP) servers and AI agentic workflows rely on standard, predictable REST APIs to inspect, query, and manipulate WordPress instances. Native endpoints allow MCP servers and connectors to seamlessly interact with Multisite—enabling AI tools to manage network state, query site data, and automate routine tasks out of the box.
Agency Workflows & Site Provisioning: Agency automation tools, custom CLI extensions, and headless deployment scripts need direct API access to spin up new sites, manage configurations, and orchestrate network operations. Native Core support eliminates the need for fragmented, third-party plugins just to perform basic programmatic site creation.
Dependencies & Roadmap
This ticket is currently blocked by official support for site meta. Once site meta lands in Core, this ticket should fast-follow immediately to provide complete, native REST coverage for Multisite network management.
I will work with @realloc to get this one landed into core.
This ticket was mentioned in Slack in #core-multisite by spacedmonkey. View the logs.
7 weeks ago
@spacedmonkey commented on PR #13055:
6 weeks ago
#24
@lloc I have put together a little PR for tests for all the meta changes -https://github.com/lloc/wordpress-develop/pull/1
#25
@
5 weeks ago
@realloc While reviewing the existing PR, I noticed something. There is a force parameter on delete that is used. For most object types, there is a not a support "trash". But looking at the wpmu_delete_blog function, there is some functionality in this function, to "drop". Not dropping, just marks the site as deleted and remove the site for all users assigned sites. This I think maps to trash functionality pretty well and I have created a little PR to add to the existing one.
https://github.com/lloc/wordpress-develop/pull/3
I am currently testing now, but other this PR looks great.
#26
@
5 weeks ago
@realloc I got a claude to test the endpoint and it found some interesting stuff. I will follow up with PRs to fix the issues.
#27
@
5 weeks ago
I have created another PR https://github.com/lloc/wordpress-develop/pull/4. This check to see if the domain / path / network id already exists when creating / editing site.
@spacedmonkey commented on PR #13055:
4 weeks ago
#28
I have more feedback here.
https://github.com/lloc/wordpress-develop/pull/4
https://github.com/lloc/wordpress-develop/pull/3
This fixes some issues that were flagged by ai.
@Biont commented on PR #13055:
2 weeks ago
#29
Quick question: Does this entirely supersede #9626? Let me know if there are any gaps or if we can simply close the other one.
@spacedmonkey commented on PR #13055:
11 days ago
#30
I spent some time hands-on testing this endpoint (via the REST API directly, plus DB-level checks) and found a critical regression in the current head (797e119c) that I think should block merging: any per-site Administrator — not just a network super admin — can delete an arbitrary site network-wide via DELETE /wp/v2/sites/{id}.
Root cause: delete_item_permissions_check() checks current_user_can( 'delete_site', $request['id'] ) (singular). Core's map_meta_cap() maps delete_site unconditionally to manage_options and doesn't actually use the $id argument for scoping — so it's not site-specific despite appearances, and manage_options is a capability every ordinary site Administrator already has. I reproduced this live: a throwaway non-super-admin Administrator account successfully deleted a site it had no relationship to.
This was actually already fixed correctly earlier in this PR's history — b447451 ("Drop is_super_admin() from sites checks") left the check as current_user_can( 'delete_sites' ) (plural, the network-only capability create_item_permissions_check()/update_item_permissions_check() already correctly use). The very last commit on the branch, 797e119c, silently changed it back to the singular delete_site form:
- if ( ! current_user_can( 'delete_sites' ) ) { + if ( ! current_user_can( 'delete_site', $request['id'] ) ) {
This also directly relates to @peterwilsoncc's earlier review comment that "the highest priority issue is the permission check[s]" on this controller, and to the "super admin should be allowed to do everything" note from my own earlier review — supers are unaffected by requiring delete_sites, since they already hold it.
Fix, tested against a local single-site and multisite install (PHPUnit both ways, phpcs, phpstan all clean): https://github.com/lloc/wordpress-develop/pull/5
That PR also includes a few related fixes/refinements found during the same testing pass:
create_item_permissions_check()now returns a specificrest_cannot_createerror instead of a bare boolean, matchingWP_REST_Posts_Controllerand this class's ownrest_cannot_edit/rest_cannot_delete.- De-duplicated the repeated multisite/capability checks into
check_multisite_support()/check_delete_permission(), reusing the existingcheck_edit_permission(). get_item_permissions_check(): default/view-context site data is now readable by members of that site (or a super admin), rather than every authenticated user regardless of membership.- Added regression tests for all of the above, including one that fails against the current
delete_site-only check.
Separately, still open (not in that PR, flagging for visibility/discussion):
create_item()/update_item()insert/update the site row before validatingmeta— a meta validation failure returns 400 but leaves the site row (or the other field changes, on update) already applied, with no rollback.metaof typearraysilently coerces a plain string into a 1-element array instead of rejecting it (inconsistent withinteger/booleanmeta, which correctly 400) — this is coreWP_REST_Meta_Fields/rest_sanitize_array()behavior, not controller-specific.- Changing
domain/pathviaPUTdoesn't resync thesiteurl/homeoptions, so the response can report a new domain whilesiteurl/homestill point at the old one. validate_domain()allows domains up to 253 chars, butwp_blogs.domainisvarchar(200)— a 253-char domain passes validation and then fails with a raw500 db_insert_errorinstead of a clean400.
Happy to open separate PRs for any of these if useful.
@peterwilsoncc commented on PR #13055:
10 days ago
#31
his was actually already fixed correctly earlier in this PR's history —
b447451("Drop is_super_admin() from sites checks") left the check ascurrent_user_can( 'delete_sites' )(plural, the network-only capabilitycreate_item_permissions_check()/update_item_permissions_check()already correctly use). The very last commit on the branch, 797e119, silently changed it back to the singulardelete_siteform:
Thanks for catching that @spacedmonkey -- that was my mistake as I presumed the singular was the meta cap for the plural. Apparently that's an exception to the common case 🤷
@spacedmonkey commented on PR #13055:
9 days ago
#32
Opened a follow-up PR against feature/40365-rest-sites-endpoint: https://github.com/lloc/wordpress-develop/pull/8
What it fixes: the wp/v2/sites REST endpoint currently scopes access checks (create_sites, manage_sites, and site membership) to the *current* network only, via is_super_admin()/get_super_admins() with no network context. That means a super admin of network B has no way to manage sites on network B through the REST API unless they're also a super admin of whichever network the request happens to be authenticated against.
This PR adds an optional $network_id parameter to is_super_admin() and get_super_admins(), so super-admin status can be checked against a specific network (falling back to each network's own site_admins option instead of always defaulting to the current network). The sites controller's permission checks (check_network_ids_exist() / check_network_access()) then use this to validate and authorize network/network_exclude request parameters on the list, single-item, create, and update routes, so a network-scoped super admin can be granted (or correctly denied) access to sites on a specific network rather than only the current one.
New PHPUnit coverage is included for the network-id validation and cross-network access behavior on those routes. Both the single-site and multisite WP_Test_REST_Sites_Controller suites pass.
🤖 Generated with Claude Code
@realloc commented on PR #13055:
9 days ago
#33
Quick question: Does this entirely supersede #9626? Let me know if there are any gaps or if we can simply close the other one.
Hey @Biont, yes, I think we can close #9626. It adds a read-only GET /sites list, and this PR covers that plus full CRUD, site meta, and tests. If you had a use case in mind that's still missing here, let us know.
@Biont commented on PR #13055:
9 days ago
#34
Hey @Biont, yes, I think we can close #9626. It adds a read-only
GET /siteslist, and this PR covers that plus full CRUD, site meta, and tests.
Love it. Thanks so much
@spacedmonkey commented on PR #13055:
9 days ago
#35
While reviewing WP_REST_Sites_Controller, I noticed delete_item_permissions_check() performed no network-ID validation at all — unlike get_items, get_item, create_item, and update_item_permissions_check(), it never called check_network_ids_exist() / check_network_access(). That means a request to delete a site on a network the requester isn't a super admin of (or an ID that doesn't correspond to a real network) would skip straight to the delete-capability check, rather than being rejected for lacking network access.
I've opened a fix here: https://github.com/lloc/wordpress-develop/pull/9
It:
- Adds the missing network-ID check to
delete_item_permissions_check(), consistent with the other four permission-check methods. - Also fixes the check ordering in
get_items_permissions_check()so network validation runs before the edit/own-user permission checks. - Merges
check_network_ids_exist()andcheck_network_access()into a singlecheck_network_ids()method to remove the duplicated two-step pattern that was repeated at all 5 call sites. - Adds PHPUnit coverage for the new delete-permission network checks.
npm run test:php -- --filter WP_Test_REST_Sites_Controller -c tests/phpunit/multisite.xml passes (154 tests, 555 assertions).
#36
@
8 days ago
- Keywords changes-requested added
The linked pull request exposes cross-network site details to unauthorized users so I'm marking this as needing changes.
jonnydmg commented on PR #13055:
7 days ago
#37
While the changes to checking and getting super admins may be helpful for a programmatic API I don't think that querying one network from another via a public API is a security risk.
For example, this current implimentation exposes cross nmetwork sites when no network is specified.
- Install and network enable WP Multi Network
- Create a new network
- Create a new user (
n2admin)- Set n2admin as a super user of the second network
- Log in to the second network as n2admin
- Observe in the dashboard "My Networks" they only have access to network 2
- In the browser console run the command
wp.apiFetch( { 'path': '/wp/v2/sites' } )- Observe the result contains details for both the network they are super admin of and the network
This is an example of why I made my earlier comment that the network should be inferred from the URL on which the request is being made. It's a security risk but, equally significantly, details of one network should not be made available from another. Doing so breaks the isolation multiple networks are intended to maintain.
That is a bug and sorry that one is me. This commit https://github.com/WordPress/wordpress-develop/pull/13055/changes/959bf3d24d75c86bebce7d7c323e6cac0a549845.
I will fix it.
@peterwilsoncc commented on PR #13055:
7 days ago
#38
@spacedmonkey @jonnydmg The endpoint just can't allow querying across network, it will introduce data exposure no matter how it's coded up
Each network is entirely independent: logins, network activated plugins, mu-plugins can be network specific via get_current_network_id(). Each site can have a different set of plugins activated too.
One network may hard code super admins via pre_site_option_{$option}, another network may not. Access on the network that does so will bypass the filter on the network that does not.
The same will occur if one network filters the manage_sites permission via the map_meta_cap filter, the other may not.
Even for sites on the same network, the plugins can differ site-to-site so the endpoint can't be available on sub-sites due to different plugins.
This is what I think needs to happen before this can be considered secure enough to commit:
- No cross-network queries, the
network_idparameter is hard coded to the current network inWP_Site_Query() - The endpoint is only available on the main site, ie
is_main_site() === true-- this matches the network admin behaviour in which/sub-site/wp-admin/networkredirects to/wp-admin/networkon the main site.
This will ensure that plugins and mu-plugins for the endpoint match what is loaded in the network admin currently.
#39
@
7 days ago
- Keywords early added
- Milestone 7.2 → Future Release
- Type task (blessed) → enhancement
I'm marking this early and moving it off the 7.2 milestone as there are a bunch of security implications that need to be considered:
- The REST request needs to run the same plugins, mu-plugins, network activated plugins as the network admin
- This limits the endpoint to being present on
is_main_site()only - Cross network queries are not possible as the logins, permission modal and plugins can differ on each network
switch_to_blog()can't help here per the docs: PHP code loaded with the originally requested site, such as code from a plugin or theme, does not switch.
The endpoint will need a thorougher review from members of the security team, especially those most familiar with multi-site and multi-network multi-sites.
@spacedmonkey commented on PR #13055:
4 days ago
#40
@spacedmonkey
- No cross-network queries, the
network_idparameter is hard coded to the current network inWP_Site_Query()- The endpoint is only available on the main site, ie
is_main_site() === true-- this matches the network admin behaviour in which/sub-site/wp-admin/networkredirects to/wp-admin/networkon the main site.This will ensure that plugins and mu-plugins for the endpoint match what is loaded in the network admin currently.
Just a heads up, jonnydmg and spacedmonkey are both me!
- I agree, I have removed this functionality, but added a single filter, so plugins could add could add this functionality in if they want to. Using filters,
rest_site_in_network( new filter ),rest_site_collection_params( to add network parameter ) andrest_site_query( to update query ) andrest_pre_insert_site( to update crud functions ). Between these filters that could be enough to resource this functionality. See https://github.com/lloc/wordpress-develop/pull/11 - Limiting to main site is not really an option. One of the other planned use for this endpoint in core is "My sites" section in the admin toolbar. This is why is there is a user_id of "me" to get my sites. If we limit this endpoint to just the main site, then we will have corrs errors.
I know there could be something on the subsite that could tweak the permsissoins. But if you have a plugin doing that, you could do a lot, like regsitering your own sites endpoint or otherwise expose site data. I am not sure this is something we can lock down. You would hope the network maintainer would lock this down.
Thoughts @peterwilsoncc @lloc ?
@realloc commented on PR #13055:
3 days ago
#41
I checked this against WP Multi Network. Peter's cross-network leak is already fixed: get_items is pinned to the current network, and site_in_network() blocks single sites on other networks.
Proposal:
- Listing,
editcontext, create/update/delete: main site only (is_main_site(), which is per network) and current network only. These then run with the same plugins as/wp-admin/network. One caveat:is_network_admin()is false in REST, so mu-plugins that check it still won't load. user=meinviewcontext: allowed on every site and across networks, which avoids the CORS issue. The toolbar already lists these sites viaget_blogs_of_user(), which ignores networks. The endpoint returns a few more fields (dates,post_count,lang_id), all harmless.GET /sites/{id}for member sites would need the same exemption for consistency.
Anyone who wants cross-network management can restore it through the rest_site_query and rest_site_in_network filters. WP Multi Network's own controller shows the pattern: switch_to_network(), then current_user_can( 'manage_network' ).
Would that work for you both?
@spacedmonkey commented on PR #13055:
3 days ago
#42
@lloc @peterwilsoncc
If we expose data to members of the site, should be consider making register meta only show if context=edit. Site meta would might have senative data in it.
@johnjamesjacoby commented on PR #13055:
3 days ago
#43
Thanks everyone for continuing to work through this. @lloc, you’ve done a great job moving a large and complicated change forward, testing it with WP Multi Network, and narrowing the remaining disagreement.
I think @peterwilsoncc is likely right that the safest boundary for this controller is:
- Current-network access only.
- Main-site access only, so requests run with the same plugin environment as /wp-admin/network/.
I am _hesitant_ to add a user=me cross-network exception. The core fields appear harmless but the response is extensible through registered site-meta, additional REST fields, and filters such as rest_prepare_site. The site-meta question raised above is one example of how the effective response can differ from the fixed fields in the controller.
I also don’t think the toolbar use-case needs to determine this controller’s security model, or necessarily needs to be implemented in core as part of this change. The toolbar already has a long history of accumulating responsibilities and blog/network queries, and my guess is we'd all prefer not to repeat some of those old design mistakes. 😆
If a client-side, cross-network "My Sites" API _is_ needed later, that would be easier to review as a separate, read-only endpoint with a deliberately small and fixed response schema. (That keeps this controller aligned with network administration while giving the toolbar use case its own security and API-design discussion.)
My personal preference is to keep this PR focused on current-network management from the network's main site, without the cross-network member exception.
Thank you again everyone for all the work getting it this far. 💗
@spacedmonkey commented on PR #13055:
2 days ago
#44
@lloc @peterwilsoncc @JJJ I've opened lloc/wordpress-develop#12 against feature/40365-rest-sites-endpoint with the changes from the discussion above. In summary:
Main site only
- Create, update, delete and single-site reads only work from the network's main site. From any other site they return a 403:
rest_cannot_{create,edit,delete,view}_not_on_main_site. - The "my sites" exceptions are kept narrow and only work in the
viewcontext: listing your own sites (user=meor your own ID), and a member reading their own site.embedis no longer exempt.
Permissions
context=editrequiresmanage_siteson both routes. It is checked before the site is looked up, so it doesn't reveal whether a site exists.- Listing another user's sites requires
manage_sites(rest_forbidden_user). - Without
manage_sites, you can't filter or order by fields you can't see (rest_forbidden_param). That coversarchived,mature,spam,deleted,lang_id,lang_id_exclude,before,afterandorderby=registered|last_updated. Otherwise the results would reveal those values.
Response and schema
- The status flags, the dates,
lang_id,post_countand the registered site meta are now edit context only. This addresses the concern @JJJ and I raised that extensible fields like meta could leak to members. - New read-only
admin_url.homeandsiteurlnow come fromget_home_url()andget_site_url(), so their filters apply. titleduplicatedblogname, so it's removed.blognameis now writable when a site is created.
user parameter
- The
userfilter and the create-onlyuser_idare now a singleuserparameter. It is validated as a positive user ID or"me", and invalid values returnrest_invalid_param.
Other
- If the user filter matches no sites, the site queries are skipped, but
rest_site_querystill fires.
All sites and site meta tests pass on multisite (254) and single site, and lint and PHPStan are clean.
Feedback welcome, particularly on:
- Whether keeping the view-context "my sites" exceptions is acceptable, given the main-site-only boundary.
- The set of edit-only fields.
- Merging
useranduser_idinto one parameter.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)
These tickets belong to our planned roadmap (a few of them not per final decision), so flagging with a keyword for better overview.