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-connection/src/class-rest-connector.php +200 -14 16.2 → 16.3 View file →
@@ -9,8 +9,9 @@
9 9
10 10 use Automattic\Jetpack\Connection\Webhooks\Authorize_Redirect;
11 11 use Automattic\Jetpack\Constants;
12 12 use Automattic\Jetpack\Redirect;
13 +use Automattic\Jetpack\Roles;
13 14 use Automattic\Jetpack\Status;
14 15 use Jetpack_XMLRPC_Server;
15 16 use WP_Error;
16 17 use WP_REST_Request;
@@ -22,9 +23,22 @@
22 23 *
23 24 * @phan-constructor-used-for-side-effects
24 25 */
25 26 class REST_Connector {
27 +
26 28 /**
29 + * Site record options left out of the site data REST response.
30 + *
31 + * @since 9.9.0.1
32 + *
33 + * @var string[]
34 + */
35 + const EXCLUDED_SITE_OPTIONS = array(
36 + 'frame_nonce',
37 + 'jetpack_frame_nonce',
38 + );
39 +
40 + /**
27 41 * The Connection Manager.
28 42 *
29 43 * @var Manager
30 44 */
@@ -343,8 +357,30 @@
343 357 ),
344 358 ),
345 359 )
346 360 );
361 +
362 + // Confirm the current user as the protected owner. Not the connection-owner change above.
363 + register_rest_route(
364 + 'jetpack/v4',
365 + '/connection/owner/protect',
366 + array(
367 + 'methods' => WP_REST_Server::EDITABLE,
368 + 'callback' => array( static::class, 'protect_connection_owner' ),
369 + 'permission_callback' => array( static::class, 'protect_connection_owner_permission_check' ),
370 + )
371 + );
372 +
373 + // Release the protected owner, leaving ownership open to any connected administrator.
374 + register_rest_route(
375 + 'jetpack/v4',
376 + '/connection/owner/release',
377 + array(
378 + 'methods' => WP_REST_Server::EDITABLE,
379 + 'callback' => array( static::class, 'release_connection_owner' ),
380 + 'permission_callback' => array( static::class, 'release_connection_owner_permission_check' ),
381 + )
382 + );
347 383 }
348 384
349 385 /**
350 386 * Handles verification that a site is registered.
@@ -743,16 +779,20 @@
743 779 return $response;
744 780 }
745 781
746 782 /**
747 - * Verify that user is allowed to disconnect Jetpack.
783 + * Verify that user is allowed to restore the connection.
748 784 *
785 + * Users with only 'jetpack_connect_user' get through, but connection_reconnect()
786 + * limits them to refreshing their own user token.
787 + *
749 788 * @since 1.15.0
789 + * @since 9.8.0 Also allows 'jetpack_connect_user'.
750 790 *
751 - * @return bool|WP_Error Whether user has the capability 'jetpack_disconnect'.
791 + * @return bool|WP_Error Whether user has the capability 'jetpack_reconnect' or 'jetpack_connect_user'.
752 792 */
753 793 public static function jetpack_reconnect_permission_check() {
754 - if ( current_user_can( 'jetpack_reconnect' ) ) {
794 + if ( current_user_can( 'jetpack_reconnect' ) || current_user_can( 'jetpack_connect_user' ) ) {
755 795 return true;
756 796 }
757 797
758 798 return new WP_Error( 'invalid_user_permission_jetpack_disconnect', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
@@ -770,8 +810,9 @@
770 810 /**
771 811 * The endpoint tried to partially or fully reconnect the website to WP.com.
772 812 *
773 813 * @since 1.15.0
814 + * @since 9.8.0 Users without 'jetpack_reconnect' only refresh their own user token.
774 815 *
775 816 * @return \WP_REST_Response|WP_Error
776 817 */
777 818 public function connection_reconnect() {
@@ -778,9 +819,11 @@
778 819 $response = array();
779 820
780 821 $next = null;
781 822
782 - $result = $this->connection->restore();
823 + $result = current_user_can( 'jetpack_reconnect' )
824 + ? $this->connection->restore()
825 + : $this->connection->refresh_user_token( false );
783 826
784 827 if ( is_wp_error( $result ) ) {
785 828 $response = $result;
786 829 } elseif ( is_string( $result ) ) {
@@ -933,20 +976,11 @@
933 976 $is_connection_owner = isset( $request['is_connection_owner'] )
934 977 ? (bool) $request['is_connection_owner']
935 978 : ( new Manager() )->get_connection_owner_id() === $user_id;
936 979
980 + // Tokens::update_user_token() fires jetpack_updated_user_token itself.
937 981 ( new Tokens() )->update_user_token( $user_id, $request['user_token'], $is_connection_owner );
938 982
939 - /**
940 - * Fires when the user token gets successfully replaced.
941 - *
942 - * @since 1.29.0
943 - *
944 - * @param int $user_id User ID.
945 - * @param string $token New user token.
946 - */
947 - do_action( 'jetpack_updated_user_token', $user_id, $request['user_token'] );
948 -
949 983 return rest_ensure_response(
950 984 array(
951 985 'success' => true,
952 986 )
@@ -1075,8 +1109,132 @@
1075 1109 return new WP_Error( 'invalid_user_permission_set_connection_owner', self::get_user_permissions_error_msg(), array( 'status' => rest_authorization_required_code() ) );
1076 1110 }
1077 1111
1078 1112 /**
1113 + * Confirm the current user as the protected owner.
1114 + *
1115 + * The claim is always for the signed-in user. A caller cannot name someone else.
1116 + *
1117 + * @since 9.9.0
1118 + *
1119 + * @return WP_REST_Response|WP_Error
1120 + */
1121 + public static function protect_connection_owner() {
1122 + $result = ( new Manager() )->set_protected_owner( get_current_user_id() );
1123 +
1124 + if ( is_wp_error( $result ) ) {
1125 + return $result;
1126 + }
1127 +
1128 + return rest_ensure_response(
1129 + array(
1130 + 'code' => 'success',
1131 + )
1132 + );
1133 + }
1134 +
1135 + /**
1136 + * Whether the current user may confirm a protected owner.
1137 + *
1138 + * A connected administrator qualifies, and only while a consumer is requesting a protected
1139 + * owner. Holding the connection owner slot does not matter.
1140 + *
1141 + * `requires_protected_owner()` is documented as a momentary answer, but it is the only opt-in
1142 + * signal there is, so a consumer that surfaces a confirmation must keep answering true for as
1143 + * long as it is on screen. One that flips to false between render and submit turns its own
1144 + * link into a 403.
1145 + *
1146 + * @since 9.9.0
1147 + *
1148 + * @return true|WP_Error
1149 + */
1150 + public static function protect_connection_owner_permission_check() {
1151 + $user_id = get_current_user_id();
1152 + $admin_cap = ( new Roles() )->translate_role_to_cap( 'administrator' );
1153 + $manager = new Manager();
1154 +
1155 + if (
1156 + $user_id
1157 + && current_user_can( 'jetpack_connect' )
1158 + && $admin_cap
1159 + && current_user_can( $admin_cap )
1160 + && $manager->is_user_connected( $user_id )
1161 + && $manager->requires_protected_owner()
1162 + ) {
1163 + return true;
1164 + }
1165 +
1166 + return new WP_Error(
1167 + 'invalid_user_permission_protect_owner',
1168 + self::get_user_permissions_error_msg(),
1169 + array( 'status' => rest_authorization_required_code() )
1170 + );
1171 + }
1172 +
1173 + /**
1174 + * Release the protected owner for this site.
1175 + *
1176 + * @since 9.9.0
1177 + *
1178 + * @return WP_REST_Response|WP_Error
1179 + */
1180 + public static function release_connection_owner() {
1181 + $result = ( new Manager() )->release_protected_owner();
1182 +
1183 + if ( is_wp_error( $result ) ) {
1184 + return $result;
1185 + }
1186 +
1187 + return rest_ensure_response(
1188 + array(
1189 + 'code' => 'success',
1190 + )
1191 + );
1192 + }
1193 +
1194 + /**
1195 + * Whether the current user may release the protected owner.
1196 + *
1197 + * Only the confirmed owner qualifies. WordPress.com is asked again before anything is cleared,
1198 + * and its answer is the one that decides.
1199 + *
1200 + * Deliberately not gated on `requires_protected_owner()`, unlike confirming: a consumer that
1201 + * has stopped asking must not strand a site holding a lock it can no longer release.
1202 + *
1203 + * @since 9.9.0
1204 + *
1205 + * @return true|WP_Error
1206 + */
1207 + public static function release_connection_owner_permission_check() {
1208 + $user_id = get_current_user_id();
1209 + $admin_cap = ( new Roles() )->translate_role_to_cap( 'administrator' );
1210 + $manager = new Manager();
1211 +
1212 + if (
1213 + $user_id
1214 + && current_user_can( 'jetpack_connect' )
1215 + && $admin_cap
1216 + && current_user_can( $admin_cap )
1217 + && $manager->is_user_connected( $user_id )
1218 + ) {
1219 + // `RE_EVALUATE` settles that the connection owner matches the anchor, so pinning this
1220 + // user to that owner is what makes it their identity. A matching binding would not:
1221 + // Premium Content writes the same key directly, so the IDs are not unique site-wide.
1222 + $state = $manager->resolve_protected_owner_state();
1223 +
1224 + if ( Manager::PO_STATE_RE_EVALUATE === $state['status'] && $user_id === (int) $manager->get_connection_owner_id() ) {
1225 + return true;
1226 + }
1227 + }
1228 +
1229 + return new WP_Error(
1230 + 'invalid_user_permission_release_owner',
1231 + self::get_user_permissions_error_msg(),
1232 + array( 'status' => rest_authorization_required_code() )
1233 + );
1234 + }
1235 +
1236 + /**
1079 1237 * The endpoint verifies blog connection and blog token validity.
1080 1238 *
1081 1239 * @since 2.7.0
1082 1240 *
@@ -1187,8 +1345,10 @@
1187 1345 public static function site_data_response( ?Manager $connection = null ) {
1188 1346 $site_data = ( $connection ?? new Manager() )->get_connected_site_data();
1189 1347
1190 1348 if ( ! is_wp_error( $site_data ) ) {
1349 + $site_data = self::exclude_site_options( $site_data );
1350 +
1191 1351 /**
1192 1352 * Fires when the site data was successfully returned from the /sites/%d wpcom endpoint.
1193 1353 *
1194 1354 * @since 8.10.0
@@ -1222,8 +1382,34 @@
1222 1382 'api_error_code' => empty( $error_data['api_error_code'] ) ? null : $error_data['api_error_code'],
1223 1383 'api_http_code' => empty( $error_data['api_http_code'] ) ? null : $error_data['api_http_code'],
1224 1384 )
1225 1385 );
1386 + }
1387 +
1388 + /**
1389 + * Removes EXCLUDED_SITE_OPTIONS from the site record before it is served to a REST caller.
1390 + *
1391 + * Works on a copy: a listener on 'jetpack_site_data_fetched', or any other internal
1392 + * consumer holding the record, keeps seeing it whole.
1393 + *
1394 + * @since 9.9.0.1
1395 + *
1396 + * @param object $site_data The decoded site record.
1397 + * @return object The record to serve, with EXCLUDED_SITE_OPTIONS removed.
1398 + */
1399 + private static function exclude_site_options( $site_data ) {
1400 + if ( ! is_object( $site_data ) || ! isset( $site_data->options ) || ! is_object( $site_data->options ) ) {
1401 + return $site_data;
1402 + }
1403 +
1404 + $site_data = clone $site_data;
1405 + $site_data->options = clone $site_data->options;
1406 +
1407 + foreach ( self::EXCLUDED_SITE_OPTIONS as $option ) {
1408 + unset( $site_data->options->$option );
1409 + }
1410 +
1411 + return $site_data;
1226 1412 }
1227 1413
1228 1414 /**
1229 1415 * Run all connection health tests and return the result.