PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-backup/src/rest/class-capabilities-bridge.php +167 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,167 @@
1 +<?php
2 +/**
3 + * Capabilities REST bridge — WPCOM's site rewind state, plus the local
4 + * facts the dashboard needs in the same breath.
5 + *
6 + * @package automattic/jetpack-backup-plugin
7 + */
8 +
9 +namespace Automattic\Jetpack\Backup\V0005\REST;
10 +
11 +use Automattic\Jetpack\Connection\Client;
12 +use WP_Error;
13 +use WP_REST_Server;
14 +
15 +if ( ! defined( 'ABSPATH' ) ) {
16 + exit( 0 );
17 +}
18 +
19 +/**
20 + * Returns what the modernized dashboard is allowed to show.
21 + *
22 + * Two halves kept apart by the response shape: the top level projects
23 + * WordPress.com's answer into the flags `<Gates>` reads, while everything under
24 + * `local` was decided on the site. They ride together because this is the one
25 + * request every screen makes before rendering a body. Add locally-derived
26 + * values under `local`, never beside the projections.
27 + */
28 +class Capabilities_Bridge {
29 +
30 + /**
31 + * Register the GET /jetpack/v4/site/capabilities route.
32 + *
33 + * @return void
34 + */
35 + public static function register_routes() {
36 + register_rest_route(
37 + 'jetpack/v4',
38 + '/site/capabilities',
39 + array(
40 + 'methods' => WP_REST_Server::READABLE,
41 + 'callback' => array( __CLASS__, 'get_capabilities' ),
42 + 'permission_callback' => array( Rest_Controller::class, 'permission_check' ),
43 + )
44 + );
45 + }
46 +
47 + /**
48 + * Proxy `/sites/{id}/rewind/capabilities` (v2, as_user) and project
49 + * the response into the shape the React layer expects.
50 + *
51 + * This is the same endpoint the legacy `/jetpack/v4/backup-capabilities`
52 + * route hits — it returns a flat `{ capabilities: [...] }` envelope.
53 + * The earlier `/rewind?force=wpcom` variant returns site *state*, not
54 + * a capabilities list, and on some plan shapes (e.g. Jetpack Complete)
55 + * the `capabilities` key is missing entirely, which produced a false
56 + * "no plan" gate for plans that do include Backup.
57 + *
58 + * The `local` branch rides along on the same response and is decided
59 + * here rather than upstream; the class docblock says why it lives on
60 + * this route and why it is nested.
61 + *
62 + * @return \WP_REST_Response|WP_Error The decoded capabilities, or WP_Error on failure.
63 + */
64 + public static function get_capabilities() {
65 + $blog_id = Rest_Controller::get_blog_id_or_error();
66 + if ( is_wp_error( $blog_id ) ) {
67 + return $blog_id;
68 + }
69 +
70 + $response = Client::wpcom_json_api_request_as_user(
71 + sprintf( '/sites/%d/rewind/capabilities', $blog_id ),
72 + 'v2',
73 + array(),
74 + null,
75 + 'wpcom'
76 + );
77 +
78 + if ( is_wp_error( $response ) ) {
79 + return Rest_Controller::transport_error( $response, 'capabilities_fetch_failed' );
80 + }
81 +
82 + // Cast: `wp_remote_retrieve_response_code()` returns whatever the
83 + // transport put there, and a numeric string fails a strict
84 + // comparison against 200 — sending a perfectly good response down
85 + // the failure branch, and reporting it as a failure rather than as
86 + // the success it was.
87 + $status_code = (int) wp_remote_retrieve_response_code( $response );
88 + if ( 200 !== $status_code ) {
89 + return Rest_Controller::upstream_error(
90 + $response,
91 + 'capabilities_fetch_failed',
92 + __( 'Could not fetch site capabilities.', 'jetpack-backup-pkg' )
93 + );
94 + }
95 +
96 + $body = json_decode( wp_remote_retrieve_body( $response ), true );
97 +
98 + // A 200 we cannot read is refused rather than projected.
99 + //
100 + // Coercing it to an empty list is the same as answering "this site
101 + // has no Backup plan", and that answer is acted on: `<Gates>`
102 + // renders the upgrade screen. So a truncated response, an HTML
103 + // error page from something in front of WordPress.com, or a shape
104 + // change upstream would each show a paying customer an advert for
105 + // what they already own, with no error anywhere to explain it.
106 + // The docblock above records this exact mechanism firing once
107 + // already; that fix repointed the endpoint and left the tolerant
108 + // projection in place.
109 + //
110 + // An *empty* list is not this case. It is a legitimate answer —
111 + // the one every site without Backup gives — and refusing it would
112 + // put a permanent error in front of precisely the people the
113 + // upgrade screen is for.
114 + // `wp_is_numeric_array()` and not `is_array()`, because the two
115 + // differ on the shape most likely to arrive if upstream drifts: a
116 + // keyed map. `is_array()` accepts `{"capabilities":{"backup":true}}`,
117 + // and `in_array()` then compares against that map's *values* — so
118 + // the site reads as having no plan, which is the outcome this
119 + // whole guard exists to prevent. It returns true for an empty
120 + // array, so the carve-out below survives.
121 + if (
122 + ! is_array( $body )
123 + || ! isset( $body['capabilities'] )
124 + || ! wp_is_numeric_array( $body['capabilities'] )
125 + ) {
126 + return new WP_Error(
127 + 'capabilities_unreadable',
128 + __( "Could not read this site's plan details.", 'jetpack-backup-pkg' ),
129 + // Deliberately not the 502 `Rest_Controller::transport_error()`
130 + // uses: the client reads 502 as "the answer went missing",
131 + // a meaning it shares with the destructive restore
132 + // mutation. Nothing was in flight here. WordPress.com
133 + // answered; we could not read what it said.
134 + array( 'status' => 500 )
135 + );
136 + }
137 +
138 + $capabilities = $body['capabilities'];
139 +
140 + return rest_ensure_response(
141 + array(
142 + 'hasBackupPlan' => in_array( 'backup', $capabilities, true ),
143 + 'hasScan' => in_array( 'scan', $capabilities, true ),
144 + // Decided here, not upstream. See the class docblock for why
145 + // these live in their own branch.
146 + 'local' => array(
147 + 'isStandalonePluginActive' => self::is_standalone_plugin_active(),
148 + ),
149 + )
150 + );
151 + }
152 +
153 + /**
154 + * Whether the standalone Jetpack VaultPress Backup plugin is active.
155 + *
156 + * Answered on the server: the modernized page emits no backup-specific global
157 + * to read, and a gate the client never decides cannot be bypassed from it.
158 + * `JETPACK_BACKUP_PLUGIN_DIR` tracks *plugin* activation, unlike the
159 + * `jetpack-backup` slug in `connectedPlugins`, which the package registers
160 + * and so would report the plugin present on the one site that lacks it.
161 + *
162 + * @return bool True when the standalone Backup plugin is active.
163 + */
164 + private static function is_standalone_plugin_active() {
165 + return defined( 'JETPACK_BACKUP_PLUGIN_DIR' );
166 + }
167 +}