PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.3
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 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-backup / src / rest / class-capabilities-bridge.php

class-capabilities-bridge.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.3, at jetpack_vendor/automattic/jetpack-backup/src/rest/class-capabilities-bridge.php

168 lines 6.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 }
168