Skip to content

Connection: add Proxy_Controller, a shared REST proxy to WordPress.com - #53098

Draft
retrofox wants to merge 7 commits into
update/connection-proxy-forward-corefrom
update/wpcom-proxy-package
Draft

retrofox wants to merge 7 commits into
update/connection-proxy-forward-corefrom
update/wpcom-proxy-package

Conversation

@retrofox

@retrofox retrofox commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes WOOA7S-2247. Built on #53154, which this PR targets until it merges.

Proposed changes

Every product that displays WordPress.com data in wp-admin ships its own REST proxy: a route, an allowlist of endpoints with a capability for each, a blog-signed Client call, a five-minute transient, and an error for the unconnected site. The monorepo has five implementations of that mechanism, and they disagree on cache keys, bypass rules, timeouts and error shapes.

This PR extracts the Premium Analytics one, the only endpoint-agnostic one, into the connection package as Automattic\Jetpack\Connection\Proxy_Controller, and makes the dashboard its first consumer. The shape follows the discussion in pdWQjU-1Ke-p2: a tunnel for dashboard products (Premium Analytics, Ads, VideoPress, WooCommerce through connection) that routes to the forward the trait already owns. Typed controllers such as Social's stay on WPCOM_REST_API_Proxy_Request; docs/proxy-controller.md states the rule for choosing.

The controller

Configured at construction with a REST namespace, a table of endpoint groups and a transient prefix, it registers <namespace>/proxy/v<version>/<endpoint>, anchors the table in the route regex, revalidates the parameter, checks the row's capability, strips routing and control params, forwards signed as the blog with the version in the path, caches a 200 and passes WordPress.com's status and body through.

  • One forward. request() calls the trait's forward_request_to_wpcom() (Connection: share one forward to WordPress.com in the proxy request trait #53154) instead of Client. Transport errors keep Client's code with a 500 status; the no_connection message is neutral.
  • Fail closed. pattern is required: '' exposes the endpoint alone, .* everything under the prefix, and a row without one is not routed and raises _doing_it_wrong(). The pattern is wrapped in a group in both regexes, so a top-level | stays anchored. manage_options is an opt-in option, off by default.
  • Cache per row. cache_ttl and cache => false on the row. A cache_bust group keys its reads by a generation counter that a successful write bumps, so parameterised reads miss too; the previous bust deleted the param-less key only.
  • Surface. Three protected seams, request(), prepare_body() and extract_forwarded_headers(), with the forwarded URL, timeout, version, base and the matched row in $opts. Everything else is private, so a subclass cannot lean on a helper that a later connection version changes. register_hooks() registers the route at once when called after rest_api_init.

The dashboard

REST\Api_Proxy_Controller becomes a table and three overrides: unsigned forwards for the posts group, the user_email body rewrite for jetpack-stats/user-feedback, and the pagination headers. The table now names each group's endpoints: of ten groups only stats stays open; analytics is reports/.*, wordads is stats or earnings, five groups are single endpoints. The dashboard opts in to manage_options, because no role grants activate_wordads. Route and namespace are unchanged; the frontend reads only no_connection, which is kept.

Tests

The mechanics are tested once, in connection, through a synthetic table: route, patterns, permissions, cache per row, generation busting, errors and the seams. The dashboard's tests drive its own table through the route: capability tiers, the endpoint matrix with the WordPress.com path each one reaches, the rejected sub-paths, the busting group, the pagination headers and the three overrides.

REST_Endpoints_Test hooks Partner explicitly: it relied on hooks leaked by earlier test classes, which the new test file exposed as a coverage drop on class-partner.php.

Next

VideoPress (#52941) and Ads (#53108) move to this API before anything merges. Follow-ups in Linear: stale-on-error, user-context forwarding with its cache column, cache on the trait.

Related product discussion/links

Does this pull request change what data or activity we track or use?

No.

Testing instructions

jp test php packages/connection
jp test php packages/premium-analytics

On a connected site with the dashboard built and Jetpack Stats active, open the Premium Analytics dashboard: every widget loads as before. In the browser console:

wp.apiFetch( {
  path: '/jetpack-premium-analytics/v1/proxy/v1.1/stats/top-posts?period=day&num=7'
} ).then( console.log );
  • It resolves with the same payload as on trunk, and a second call is served from the cache: wp transient list --search='jetpack-premium-analytics_proxy_*' lists one entry per distinct query.
  • /jetpack-premium-analytics/v1/proxy/v1.1/posts/<post id>/likes still resolves unsigned; /jetpack-premium-analytics/v1/proxy/v2/media and /jetpack-premium-analytics/v1/proxy/v1.1/wordads/settings are rejected with rest_no_route.
  • As a user with view_stats but not manage_options, the stats call resolves and an analytics call is rejected with a 403.
  • With the site disconnected (wp jetpack disconnect blog), the call fails with no_connection and the dashboard shows its reconnect state.

configurable WordPress.com REST proxy: allowlist, capability, cache
the dashboard keeps its table and three overrides; route unchanged
@retrofox retrofox added Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Status] In Progress labels Oct 2, 2026
@retrofox retrofox self-assigned this Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the update/wpcom-proxy-package branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack update/wpcom-proxy-package
bin/jetpack-downloader test jetpack-mu-wpcom-plugin update/wpcom-proxy-package

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Jetpack plugin:

The Jetpack plugin has different release cadences depending on the platform:

  • WordPress.com Simple releases happen as soon as you deploy your changes after merging this PR (PCYsg-Jjm-p2).
  • WoA releases happen weekly.
  • Releases to self-hosted sites happen monthly:
    • Scheduled release: October 6, 2026

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.


Premium Analytics plugin:

No scheduled milestone found for this plugin.

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@jp-launch-control

jp-launch-control Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 2 files.

File Coverage Δ% Δ Uncovered
projects/packages/connection/src/class-partner.php 16/35 (45.71%) -40.00% 14 💔
projects/packages/premium-analytics/src/REST/class-api-proxy-controller.php 48/50 (96.00%) 1.78% -11 💚

1 file is newly checked for coverage.

File Coverage
projects/packages/connection/src/class-proxy-controller.php 180/186 (96.77%) 💚

Full summary · PHP report · JS report

If appropriate, add one of these labels to override the failing coverage check: Covered by non-unit tests Use to ignore the Code coverage requirement check when E2Es or other non-unit tests cover the code Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR I don't care about code coverage for this PR Use this label to ignore the check for insufficient code coveage.

@retrofox retrofox added [Status] Needs Review This PR is ready for review. and removed [Status] In Progress labels Oct 2, 2026
no new package: Proxy_Controller next to the trait it generalizes
@retrofox retrofox changed the title Add jetpack-wpcom-proxy, the shared WordPress.com proxy package Connection: add Proxy_Controller, a shared REST proxy to WordPress.com Oct 2, 2026
the dashboard drops the mechanics tests connection now owns
@retrofox
retrofox marked this pull request as ready for review October 2, 2026 16:35
@retrofox
retrofox requested review from a team as code owners October 2, 2026 16:35

@chihsuan chihsuan 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.

Nice work! @retrofox Putting these proxies in one place makes a lot of sense. I left a few inline notes.

Just curious how this will look in #52941 and #53108. Both drafts still add their own cleanup hook, so it might be nice to move them over before this merges and see if the API needs any tweaks.

*
* @since $$next-version$$
*/
class Proxy_Controller extends WP_REST_Controller {

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.

Would it be worth making the helpers private, so only the three documented seams can be overridden? Different plugins can load different versions of connection, so changing a protected method later could break an older subclass.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. The rework on top of #53154 makes everything private except the three seams and passes what they need through $opts.

return false;
}

if ( isset( $config['pattern'] ) && ! preg_match( '#^' . preg_quote( $prefix, '#' ) . '/' . $config['pattern'] . '$#i', rtrim( $value, '/' ) ) ) {

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.

Could we wrap pattern in a group here and in the route regex? A pattern with a top-level | isn't anchored on both ends, so a request could reach endpoints the table meant to block. No current table does this, though.

Suggested change
if ( isset( $config['pattern'] ) && ! preg_match( '#^' . preg_quote( $prefix, '#' ) . '/' . $config['pattern'] . '$#i', rtrim( $value, '/' ) ) ) {
if ( isset( $config['pattern'] ) && ! preg_match( '#^' . preg_quote( $prefix, '#' ) . '/(?:' . $config['pattern'] . ')$#i', rtrim( $value, '/' ) ) ) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. It lands in the rework, in both regexes.

| --- | --- | --- |
| `capability` | yes | Capability that reads the group. `manage_options` always reads. A missing value admits administrators only. |
| `pattern` | no | Regex the sub-path must match in full, for a group that exposes specific endpoints only. Anchored in the route regex and re-checked on the request param. |
| `writes` | no | Sub-paths reachable with `POST`, the only write method. A matcher ending in `/` covers everything under it; otherwise it covers that endpoint only. |

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.

nit: Could we mention that writes entries include the group key? The docs say "sub-paths", but a sub-path alone never matches, so every POST would get a 405.

Suggested change
| `writes` | no | Sub-paths reachable with `POST`, the only write method. A matcher ending in `/` covers everything under it; otherwise it covers that endpoint only. |
| `writes` | no | Endpoints reachable with `POST`, the only write method, including the group key (`stats/referrers/spam/`). A matcher ending in `/` covers everything under it; otherwise it covers that endpoint only. |

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Taking the suggestion in the rework.

@retrofox

retrofox commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

Both drafts still add their own cleanup hook

Both drafts construct the controller on rest_api_init, so register_hooks() never runs on cron and each re-adds the cleanup filter. The rework adds a static register() and documents registering at load; #52941 and #53108 move to it before this merges.

@retrofox
retrofox marked this pull request as draft October 5, 2026 10:51
@retrofox
retrofox changed the base branch from trunk to update/connection-proxy-forward-core October 5, 2026 11:10
@retrofox
retrofox added this pull request to stack #53161 October 5, 2026 12:14
*/
protected function get_forwarded_params( WP_REST_Request $request ): array {
$params = $request->get_query_params();
unset( $params['rest_route'], $params['_locale'], $params['site'], $params['endpoint'], $params['version'], $params['force_refresh'] );

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.

An encoded _method parameter survives this filter and becomes a method override in the signed outbound request. I tested the controller locally with HTTP intercepted: an incoming GET produced an outbound GET with _method=PUT, which WordPress REST uses to dispatch the request as PUT. This means the effective upstream method can bypass the local write allowlist, although I have not executed an upstream write. Could we reject method overrides here and add a regression test that checks the final outbound request?

$alternatives = array();

foreach ( $this->prefix_config as $prefix => $config ) {
$suffix = isset( $config['pattern'] ) ? '/' . $config['pattern'] : '(?:/.*)?';

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.

A prefix without a pattern exposes every GET sub-path under that prefix, including endpoints added upstream after the consumer ships. I would expect the product to declare its intended endpoint scope and the shared controller to enforce it. Could unrestricted sub-paths require an explicit opt-in?

This branch has not been deployed

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

Labels

Docs Enhancement Changes to an existing feature — removing, adding, or changing parts of it [Package] Connection [Package] Premium Analytics [Plugin] Jetpack Issues about the Jetpack plugin. https://wordpress.org/plugins/jetpack/ [Plugin] Premium Analytics [Status] In Progress [Tests] Includes Tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants