PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta
16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / json-endpoints / jetpack / class.jetpack-json-api-plugins-replace-endpoint.php
jetpack / json-endpoints / jetpack Last commit date
class-jetpack-json-api-attachment-ownership-trait.php 3 months ago class-jetpack-json-api-delete-backup-helper-script-endpoint.php 8 months ago class-jetpack-json-api-install-backup-helper-script-endpoint.php 8 months ago class-jetpack-json-api-modules-list-v1-2-endpoint.php 8 months ago class.jetpack-json-api-check-capabilities-endpoint.php 8 months ago class.jetpack-json-api-core-endpoint.php 8 months ago class.jetpack-json-api-core-modify-endpoint.php 2 months ago class.jetpack-json-api-cron-endpoint.php 8 months ago class.jetpack-json-api-endpoint.php 2 months ago class.jetpack-json-api-get-comment-backup-endpoint.php 8 months ago class.jetpack-json-api-get-database-object-backup-endpoint.php 8 months ago class.jetpack-json-api-get-option-backup-endpoint.php 8 months ago class.jetpack-json-api-get-post-backup-endpoint.php 8 months ago class.jetpack-json-api-get-term-backup-endpoint.php 8 months ago class.jetpack-json-api-get-user-backup-endpoint.php 8 months ago class.jetpack-json-api-jps-woocommerce-connect-endpoint.php 8 months ago class.jetpack-json-api-log-endpoint.php 8 months ago class.jetpack-json-api-maybe-auto-update-endpoint.php 8 months ago class.jetpack-json-api-modules-endpoint.php 8 months ago class.jetpack-json-api-modules-get-endpoint.php 8 months ago class.jetpack-json-api-modules-list-endpoint.php 8 months ago class.jetpack-json-api-modules-modify-endpoint.php 8 months ago class.jetpack-json-api-plugins-delete-endpoint.php 8 months ago class.jetpack-json-api-plugins-endpoint.php 2 months ago class.jetpack-json-api-plugins-get-endpoint.php 8 months ago class.jetpack-json-api-plugins-install-endpoint.php 8 months ago class.jetpack-json-api-plugins-list-endpoint.php 1 month ago class.jetpack-json-api-plugins-modify-endpoint.php 8 months ago class.jetpack-json-api-plugins-modify-v1-2-endpoint.php 8 months ago class.jetpack-json-api-plugins-new-endpoint.php 8 months ago class.jetpack-json-api-plugins-replace-endpoint.php 3 months ago class.jetpack-json-api-sync-endpoint.php 2 months ago class.jetpack-json-api-themes-active-endpoint.php 8 months ago class.jetpack-json-api-themes-delete-endpoint.php 8 months ago class.jetpack-json-api-themes-endpoint.php 2 months ago class.jetpack-json-api-themes-get-endpoint.php 8 months ago class.jetpack-json-api-themes-install-endpoint.php 8 months ago class.jetpack-json-api-themes-list-endpoint.php 8 months ago class.jetpack-json-api-themes-modify-endpoint.php 8 months ago class.jetpack-json-api-themes-new-endpoint.php 8 months ago class.jetpack-json-api-themes-replace-endpoint.php 3 months ago class.jetpack-json-api-translations-endpoint.php 8 months ago class.jetpack-json-api-translations-modify-endpoint.php 8 months ago class.jetpack-json-api-updates-status-endpoint.php 2 months ago class.jetpack-json-api-user-connect-endpoint.php 8 months ago class.jetpack-json-api-user-create-endpoint.php 2 months ago class.wpcom-json-api-get-option-endpoint.php 2 months ago class.wpcom-json-api-update-option-endpoint.php 8 months ago json-api-jetpack-endpoints.php 3 months ago
class.jetpack-json-api-plugins-replace-endpoint.php
271 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3 use Automattic\Jetpack\Automatic_Install_Skin;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit( 0 );
7 }
8
9 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
10 require_once ABSPATH . 'wp-admin/includes/file.php';
11
12 /**
13 * Install-or-replace a plugin via zip upload. Passes overwrite_package=true to
14 * Plugin_Upgrader so an existing plugin at the same slug is replaced in place,
15 * mirroring wp-admin's "Replace current with uploaded" confirmation flow.
16 *
17 * POST /sites/%s/plugins/replace
18 *
19 * ## Authentication trust model
20 *
21 * Two auth modes are supported:
22 *
23 * 1. User auth — the request is mapped to a WordPress user who must hold
24 * `install_plugins` AND `update_plugins`. The ownership check verifies the
25 * referenced attachment was authored by that same user, preventing the
26 * endpoint from being used to touch another user's attachments.
27 *
28 * 2. Site-based auth (`allow_jetpack_site_auth => true`) — no user is
29 * identified; capability and ownership checks are skipped. The wpcom
30 * upload forwarder is expected to (a) vouch for the caller, (b) create the
31 * attachment as part of the same request's upload-intercept pipeline, and
32 * (c) pass the resulting ID through unchanged. If that contract is broken
33 * on the wpcom side, any trusted site credential becomes install-anything
34 * on the site.
35 *
36 * ## Cross-user attachment-deletion guard
37 *
38 * `Jetpack_JSON_API_Plugins_New_Endpoint::validate_call` deletes the referenced
39 * attachment on capability-check failure without verifying the caller owns it.
40 * This Replace endpoint overrides `validate_call` to run the ownership check
41 * first, so a low-privilege caller cannot use it to hard-delete another user's
42 * attachment as a side-effect of the cap check failing. The parent's behavior
43 * is unchanged for the legitimate case (caller's own attachment is cleaned up
44 * when their cap check fails).
45 *
46 * @phan-constructor-used-for-side-effects
47 */
48 class Jetpack_JSON_API_Plugins_Replace_Endpoint extends Jetpack_JSON_API_Plugins_New_Endpoint {
49 use Jetpack_JSON_API_Attachment_Ownership_Trait;
50
51 /**
52 * Replace is destructive, so require both install and update caps.
53 *
54 * @var array
55 */
56 protected $needed_capabilities = array( 'install_plugins', 'update_plugins' );
57
58 /**
59 * Error codes we are willing to surface to the caller. Anything outside
60 * this list is collapsed to 'install_failed' with a generic message to
61 * avoid leaking filesystem paths from Plugin_Upgrader internals.
62 *
63 * Kept aligned with codes actually emitted by `WP_Upgrader` /
64 * `Plugin_Upgrader` — $this->strings[] message keys are NOT error codes
65 * and do not belong here.
66 *
67 * @var array
68 */
69 protected static $allowed_error_codes = array(
70 'no_package',
71 'bad_request',
72 'files_not_writable',
73 'copy_dir_failed',
74 'remove_old_failed',
75 'source_read_failed',
76 'new_source_read_failed',
77 'mkdir_failed_destination',
78 'folder_exists',
79 'incompatible_archive',
80 'incompatible_archive_no_plugins',
81 'incompatible_archive_empty',
82 'incompatible_php_required_version',
83 'incompatible_wp_required_version',
84 'unable_to_connect_to_filesystem',
85 'fs_unavailable',
86 'fs_error',
87 'fs_no_plugins_dir',
88 'fs_no_folder',
89 'fs_no_root_dir',
90 'fs_no_content_dir',
91 'fs_no_temp_backup_dir',
92 'fs_temp_backup_mkdir',
93 'fs_temp_backup_move',
94 );
95
96 /**
97 * Install, replacing any existing plugin at the same slug.
98 *
99 * @return bool|WP_Error
100 */
101 public function install() {
102 $args = $this->input();
103
104 if ( ! isset( $args['zip'][0]['id'] ) || ! is_scalar( $args['zip'][0]['id'] ) ) {
105 return new WP_Error( 'no_plugin_installed', __( 'No plugin zip file was provided.', 'jetpack' ), 400 );
106 }
107
108 $expected_slug = isset( $args['slug'] ) && is_scalar( $args['slug'] )
109 ? strtolower( (string) $args['slug'] )
110 : '';
111 if ( ! preg_match( '/^[a-z0-9][a-z0-9_-]*$/', $expected_slug ) ) {
112 return new WP_Error( 'missing_slug', __( 'A valid plugin slug is required; the replace endpoint refuses to overwrite a plugin whose slug the caller has not declared.', 'jetpack' ), 400 );
113 }
114
115 $plugin_attachment_id = (int) $args['zip'][0]['id'];
116
117 // Re-checked here so direct invocations of install() (notably the test stubs)
118 // can't accidentally bypass the ownership guard that validate_call() runs on
119 // the live request path.
120 $ownership = $this->validate_attachment_ownership( $plugin_attachment_id );
121 if ( is_wp_error( $ownership ) ) {
122 return $ownership;
123 }
124
125 $zip_check = $this->validate_attachment_is_zip( $plugin_attachment_id );
126 if ( is_wp_error( $zip_check ) ) {
127 wp_delete_attachment( $plugin_attachment_id, true );
128 return $zip_check;
129 }
130
131 $local_file = get_attached_file( $plugin_attachment_id );
132 if ( ! $local_file ) {
133 wp_delete_attachment( $plugin_attachment_id, true );
134 return new WP_Error( 'local-file-does-not-exist', __( 'Uploaded plugin zip could not be found on disk.', 'jetpack' ), 400 );
135 }
136
137 $skin = new Automatic_Install_Skin();
138 $upgrader = new Plugin_Upgrader( $skin );
139
140 $result = $upgrader->install(
141 $local_file,
142 array( 'overwrite_package' => true )
143 );
144
145 wp_delete_attachment( $plugin_attachment_id, true );
146
147 if ( is_wp_error( $result ) ) {
148 return $this->sanitize_upgrader_error( $result );
149 }
150
151 if ( ! $result ) {
152 $error_code = $skin->get_main_error_code();
153 if ( 'download_failed' === $error_code ) {
154 $error_code = 'no_package';
155 }
156 if ( empty( $error_code ) || ! in_array( $error_code, self::$allowed_error_codes, true ) ) {
157 $error_code = 'install_failed';
158 }
159 return new WP_Error( $error_code, __( 'Plugin installation failed.', 'jetpack' ), 400 );
160 }
161
162 $plugin_file = $upgrader->plugin_info();
163 if ( empty( $plugin_file ) ) {
164 return new WP_Error( 'plugin_replace_info_missing', __( 'Plugin was installed but its identifier could not be determined.', 'jetpack' ), 500 );
165 }
166
167 // `overwrite_package=true` trusts the zip's own folder name, so a zip whose
168 // top-level folder differs from the declared slug would clobber an unrelated
169 // plugin. Verify the post-install identifier matches the caller's contract.
170 $installed_slug = dirname( $plugin_file );
171 if ( '.' === $installed_slug || strtolower( $installed_slug ) !== $expected_slug ) {
172 return new WP_Error( 'slug_mismatch', __( 'The installed plugin does not match the declared slug.', 'jetpack' ), 400 );
173 }
174
175 $this->plugins = array( $plugin_file );
176 $this->log[ $plugin_file ] = $upgrader->skin->get_upgrade_messages();
177
178 return true;
179 }
180
181 /**
182 * See class docblock — runs the attachment-ownership check before delegating
183 * to the parent's validate_call(), which deletes the referenced attachment
184 * on capability-check failure without verifying ownership.
185 *
186 * @param int $_blog_id Blog ID.
187 * @param string $capability Capability.
188 * @param bool $check_manage_active Whether to check manage-is-active.
189 * @return bool|WP_Error
190 */
191 protected function validate_call( $_blog_id, $capability, $check_manage_active = true ) {
192 $args = $this->input();
193 if ( isset( $args['zip'][0]['id'] ) && is_scalar( $args['zip'][0]['id'] ) ) {
194 $ownership = $this->validate_attachment_ownership( (int) $args['zip'][0]['id'] );
195 if ( is_wp_error( $ownership ) ) {
196 return $ownership;
197 }
198 }
199 return parent::validate_call( $_blog_id, $capability, $check_manage_active );
200 }
201
202 /**
203 * Collapse unknown error codes and strip potentially path-leaking messages
204 * from WP_Error instances returned by Plugin_Upgrader.
205 *
206 * @param WP_Error $error Raw upgrader error.
207 * @return WP_Error
208 */
209 protected function sanitize_upgrader_error( WP_Error $error ) {
210 $code = $error->get_error_code();
211 if ( empty( $code ) || ! in_array( $code, self::$allowed_error_codes, true ) ) {
212 return new WP_Error( 'install_failed', __( 'Plugin installation failed.', 'jetpack' ), 400 );
213 }
214 return new WP_Error( $code, __( 'Plugin installation failed.', 'jetpack' ), 400 );
215 }
216 }
217
218 // POST /sites/%s/plugins/replace
219 new Jetpack_JSON_API_Plugins_Replace_Endpoint(
220 array(
221 'description' => 'Install or replace a plugin on a Jetpack site by uploading a zip file. If a plugin with the same slug is already installed, its destination folder is replaced in place, mirroring wp-admin\'s "Replace current with uploaded" upload flow.',
222 'group' => '__do_not_document',
223 'stat' => 'plugins:replace',
224 'min_version' => '1',
225 'max_version' => '1.1',
226 'method' => 'POST',
227 'path' => '/sites/%s/plugins/replace',
228 'path_labels' => array(
229 '$site' => '(int|string) Site ID or domain',
230 ),
231 'request_format' => array(
232 'zip' => '(array) Reference to an uploaded plugin package zip file.',
233 'slug' => '(string) The plugin slug the uploaded zip must resolve to. Required; the endpoint rejects zips whose top-level folder does not match.',
234 ),
235 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format,
236 'allow_jetpack_site_auth' => true,
237 'example_request_data' => array(
238 'headers' => array(
239 'authorization' => 'Bearer YOUR_API_TOKEN',
240 ),
241 ),
242 'example_request' => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/replace',
243 )
244 );
245
246 new Jetpack_JSON_API_Plugins_Replace_Endpoint(
247 array(
248 'description' => 'Install or replace a plugin on a Jetpack site by uploading a zip file. If a plugin with the same slug is already installed, its destination folder is replaced in place, mirroring wp-admin\'s "Replace current with uploaded" upload flow.',
249 'group' => '__do_not_document',
250 'stat' => 'plugins:replace',
251 'min_version' => '1.2',
252 'method' => 'POST',
253 'path' => '/sites/%s/plugins/replace',
254 'path_labels' => array(
255 '$site' => '(int|string) Site ID or domain',
256 ),
257 'request_format' => array(
258 'zip' => '(array) Reference to an uploaded plugin package zip file.',
259 'slug' => '(string) The plugin slug the uploaded zip must resolve to. Required; the endpoint rejects zips whose top-level folder does not match.',
260 ),
261 'response_format' => Jetpack_JSON_API_Plugins_Endpoint::$_response_format_v1_2,
262 'allow_jetpack_site_auth' => true,
263 'example_request_data' => array(
264 'headers' => array(
265 'authorization' => 'Bearer YOUR_API_TOKEN',
266 ),
267 ),
268 'example_request' => 'https://public-api.wordpress.com/rest/v1.2/sites/example.wordpress.org/plugins/replace',
269 )
270 );
271