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-sync/src/modules/class-plugins.php +473 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,473 @@
1 +<?php
2 +/**
3 + * Plugins sync module.
4 + *
5 + * @package automattic/jetpack-sync
6 + */
7 +
8 +namespace Automattic\Jetpack\Sync\Modules;
9 +
10 +use Automattic\Jetpack\Constants as Jetpack_Constants;
11 +use WP_Error;
12 +
13 +if ( ! defined( 'ABSPATH' ) ) {
14 + exit( 0 );
15 +}
16 +
17 +/**
18 + * Class to handle sync for plugins.
19 + */
20 +class Plugins extends Module {
21 + /**
22 + * Action handler callable.
23 + *
24 + * @access private
25 + *
26 + * @var callable
27 + */
28 + private $action_handler;
29 +
30 + /**
31 + * Information about plugins we store temporarily.
32 + *
33 + * @access private
34 + *
35 + * @var array
36 + */
37 + private $plugin_info = array();
38 +
39 + /**
40 + * List of all plugins in the installation.
41 + *
42 + * @access private
43 + *
44 + * @var array
45 + */
46 + private $plugins = array();
47 +
48 + /**
49 + * List of all updated plugins.
50 + *
51 + * @access private
52 + *
53 + * @var array
54 + */
55 + private $plugins_updated = array();
56 +
57 + /**
58 + * List of plugins installed during this request.
59 + *
60 + * @access private
61 + *
62 + * @var array
63 + */
64 + private $plugins_installed = array();
65 +
66 + /**
67 + * List of all plugin update failures during this request.
68 + *
69 + * @access private
70 + *
71 + * @var array
72 + */
73 + private $plugins_update_failures = array();
74 +
75 + /**
76 + * State
77 + *
78 + * @access private
79 + *
80 + * @var array
81 + */
82 + private $state = array();
83 +
84 + /**
85 + * Sync module name.
86 + *
87 + * @access public
88 + *
89 + * @return string
90 + */
91 + public function name() {
92 + return 'plugins';
93 + }
94 +
95 + /**
96 + * Initialize plugins action listeners.
97 + *
98 + * @access public
99 + *
100 + * @param callable $callable Action handler callable.
101 + */
102 + public function init_listeners( $callable ) {
103 + $this->action_handler = $callable;
104 +
105 + add_action( 'deleted_plugin', array( $this, 'deleted_plugin' ), 10, 2 );
106 + add_action( 'activated_plugin', $callable, 10, 2 );
107 + add_action( 'deactivated_plugin', $callable, 10, 2 );
108 + add_action( 'delete_plugin', array( $this, 'delete_plugin' ) );
109 + add_filter( 'upgrader_pre_install', array( $this, 'populate_plugins' ), 10, 1 );
110 + add_action( 'upgrader_process_complete', array( $this, 'on_upgrader_completion' ), 10, 2 );
111 + add_action( 'jetpack_plugin_installed', $callable, 10, 1 );
112 + add_action( 'jetpack_plugin_update_failed', $callable, 10, 4 );
113 + add_action( 'jetpack_plugins_updated', $callable, 10, 2 );
114 + add_action( 'jetpack_edited_plugin', $callable, 10, 2 );
115 + add_action( 'wp_ajax_edit-theme-plugin-file', array( $this, 'plugin_edit_ajax' ), 0 );
116 +
117 + // Note that we don't simply 'expand_plugin_data' on the 'delete_plugin' action here because the plugin file is deleted when that action finishes.
118 + add_filter( 'jetpack_sync_before_enqueue_activated_plugin', array( $this, 'expand_plugin_data' ) );
119 + add_filter( 'jetpack_sync_before_enqueue_deactivated_plugin', array( $this, 'expand_plugin_data' ) );
120 + }
121 +
122 + /**
123 + * Fetch and populate all current plugins before upgrader installation.
124 + *
125 + * @access public
126 + *
127 + * @param bool|WP_Error $response Install response, true if successful, WP_Error if not.
128 + */
129 + public function populate_plugins( $response ) {
130 + if ( ! function_exists( 'get_plugins' ) ) {
131 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
132 + }
133 + $this->plugins = get_plugins();
134 + return $response;
135 + }
136 +
137 + /**
138 + * Handler for the upgrader success finishes.
139 + *
140 + * @access public
141 + *
142 + * @param \WP_Upgrader $upgrader Upgrader instance.
143 + * @param array $details Array of bulk item update data.
144 + */
145 + public function on_upgrader_completion( $upgrader, $details ) {
146 + if ( ! isset( $details['type'] ) ) {
147 + return;
148 + }
149 + if ( 'plugin' !== $details['type'] ) {
150 + return;
151 + }
152 +
153 + if ( ! isset( $details['action'] ) ) {
154 + return;
155 + }
156 +
157 + $plugins = ( $details['plugins'] ?? null );
158 + if ( empty( $plugins ) ) {
159 + $plugins = ( isset( $details['plugin'] ) ? array( $details['plugin'] ) : null );
160 + }
161 +
162 + // For plugin installer.
163 + if ( empty( $plugins ) && method_exists( $upgrader, 'plugin_info' ) ) {
164 + // @phan-suppress-next-line PhanUndeclaredMethod -- Checked above. See also https://github.com/phan/phan/issues/1204.
165 + $plugins = array( $upgrader->plugin_info() );
166 + }
167 +
168 + if ( empty( $plugins ) ) {
169 + return; // We shouldn't be here.
170 + }
171 +
172 + switch ( $details['action'] ) {
173 + case 'update':
174 + $this->state = array(
175 + 'is_autoupdate' => Jetpack_Constants::is_true( 'JETPACK_PLUGIN_AUTOUPDATE' ),
176 + );
177 + $errors = $this->get_errors( $upgrader->skin );
178 + if ( $errors ) {
179 + foreach ( $plugins as $slug ) { // Accumulate failures and defer to shutdown, to reduce request-time lag.
180 + $this->plugins_update_failures[] = array(
181 + 'plugin' => $this->get_plugin_info( $slug ),
182 + 'code' => $errors['code'],
183 + 'message' => $errors['message'],
184 + 'state' => $this->state,
185 + );
186 + }
187 + if ( ! has_action( 'shutdown', array( $this, 'sync_plugins_update_failed' ) ) ) {
188 + add_action( 'shutdown', array( $this, 'sync_plugins_update_failed' ), 9 );
189 + }
190 +
191 + return;
192 + }
193 +
194 + $this->plugins_updated = array_map( array( $this, 'get_plugin_info' ), $plugins );
195 + add_action( 'shutdown', array( $this, 'sync_plugins_updated' ), 9 );
196 +
197 + break;
198 + case 'install':
199 + // Accumulate installs and defer to shutdown.
200 + $this->plugins_installed = array_merge(
201 + $this->plugins_installed,
202 + array_map( array( $this, 'get_plugin_info' ), $plugins )
203 + );
204 + if ( ! has_action( 'shutdown', array( $this, 'sync_plugins_installed' ) ) ) {
205 + add_action( 'shutdown', array( $this, 'sync_plugins_installed' ), 9 );
206 + }
207 +
208 + break;
209 + }
210 + }
211 +
212 + /**
213 + * Retrieve the plugin information by a plugin slug.
214 + *
215 + * @access private
216 + *
217 + * @param string $slug Plugin slug.
218 + * @return array Plugin information.
219 + */
220 + private function get_plugin_info( $slug ) {
221 + $plugins = get_plugins(); // Get the most up to date info.
222 + if ( isset( $plugins[ $slug ] ) ) {
223 + return array_merge( array( 'slug' => $slug ), $plugins[ $slug ] );
224 + }
225 + // Try grabbing the info from before the update.
226 + return isset( $this->plugins[ $slug ] ) ? array_merge( array( 'slug' => $slug ), $this->plugins[ $slug ] ) : array( 'slug' => $slug );
227 + }
228 +
229 + /**
230 + * Retrieve upgrade errors.
231 + *
232 + * @access private
233 + *
234 + * @param \Automatic_Upgrader_Skin|\WP_Upgrader_Skin $skin The upgrader skin being used.
235 + * @return array|boolean Error on error, false otherwise.
236 + */
237 + private function get_errors( $skin ) {
238 + // @phan-suppress-next-line PhanUndeclaredMethod -- Checked before being called. See also https://github.com/phan/phan/issues/1204.
239 + $errors = method_exists( $skin, 'get_errors' ) ? $skin->get_errors() : null;
240 + if ( is_wp_error( $errors ) ) {
241 + $error_code = $errors->get_error_code();
242 + if ( ! empty( $error_code ) ) {
243 + return array(
244 + 'code' => $error_code,
245 + 'message' => $errors->get_error_message(),
246 + );
247 + }
248 + }
249 +
250 + if ( isset( $skin->result ) ) {
251 + $errors = $skin->result;
252 + if ( is_wp_error( $errors ) ) {
253 + return array(
254 + 'code' => $errors->get_error_code(),
255 + 'message' => $errors->get_error_message(),
256 + );
257 + }
258 +
259 + if ( empty( $skin->result ) ) {
260 + return array(
261 + 'code' => 'unknown',
262 + 'message' => __( 'Unknown Plugin Update Failure', 'jetpack-sync' ),
263 + );
264 + }
265 + }
266 + return false;
267 + }
268 +
269 + /**
270 + * Handle plugin ajax edit in the administration.
271 + *
272 + * @access public
273 + *
274 + * @todo Update this method to use WP_Filesystem instead of fopen/fclose.
275 + */
276 + public function plugin_edit_ajax() {
277 + // This validation is based on wp_edit_theme_plugin_file().
278 + if ( empty( $_POST['file'] ) ) {
279 + return;
280 + }
281 +
282 + $file = wp_unslash( $_POST['file'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated manually just after.
283 + if ( 0 !== validate_file( $file ) ) {
284 + return;
285 + }
286 +
287 + if ( ! isset( $_POST['newcontent'] ) ) {
288 + return;
289 + }
290 +
291 + if ( ! isset( $_POST['nonce'] ) ) {
292 + return;
293 + }
294 +
295 + if ( empty( $_POST['plugin'] ) ) {
296 + return;
297 + }
298 +
299 + $plugin = wp_unslash( $_POST['plugin'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated manually just after.
300 + if ( ! current_user_can( 'edit_plugins' ) ) {
301 + return;
302 + }
303 +
304 + if ( ! wp_verify_nonce( $_POST['nonce'], 'edit-plugin_' . $file ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- WP core doesn't pre-sanitize nonces either.
305 + return;
306 + }
307 + $plugins = get_plugins();
308 + if ( ! array_key_exists( $plugin, $plugins ) ) {
309 + return;
310 + }
311 +
312 + if ( 0 !== validate_file( $file, get_plugin_files( $plugin ) ) ) {
313 + return;
314 + }
315 +
316 + $real_file = WP_PLUGIN_DIR . '/' . $file;
317 +
318 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable
319 + if ( ! is_writable( $real_file ) ) {
320 + return;
321 + }
322 +
323 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
324 + $file_pointer = fopen( $real_file, 'w+' );
325 + if ( false === $file_pointer ) {
326 + return;
327 + }
328 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
329 + fclose( $file_pointer );
330 + /**
331 + * This action is documented already in this file
332 + */
333 + do_action( 'jetpack_edited_plugin', $plugin, $plugins[ $plugin ] );
334 + }
335 +
336 + /**
337 + * Handle plugin deletion.
338 + *
339 + * @access public
340 + *
341 + * @param string $plugin_path Path to the plugin main file.
342 + */
343 + public function delete_plugin( $plugin_path ) {
344 + $full_plugin_path = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $plugin_path;
345 +
346 + // Checking for file existence because some sync plugin module tests simulate plugin installation and deletion without putting file on disk.
347 + if ( file_exists( $full_plugin_path ) ) {
348 + $all_plugin_data = get_plugin_data( $full_plugin_path );
349 + $data = array(
350 + 'name' => $all_plugin_data['Name'],
351 + 'version' => $all_plugin_data['Version'],
352 + );
353 + } else {
354 + $data = array(
355 + 'name' => $plugin_path,
356 + 'version' => 'unknown',
357 + );
358 + }
359 +
360 + $this->plugin_info[ $plugin_path ] = $data;
361 + }
362 +
363 + /**
364 + * Invoked after plugin deletion.
365 + *
366 + * @access public
367 + *
368 + * @param string $plugin_path Path to the plugin main file.
369 + * @param boolean $is_deleted Whether the plugin was deleted successfully.
370 + */
371 + public function deleted_plugin( $plugin_path, $is_deleted ) {
372 + call_user_func( $this->action_handler, $plugin_path, $is_deleted, $this->plugin_info[ $plugin_path ] );
373 + unset( $this->plugin_info[ $plugin_path ] );
374 + }
375 +
376 + /**
377 + * Expand the plugins within a hook before they are serialized and sent to the server.
378 + *
379 + * @access public
380 + *
381 + * @param array $args The hook parameters.
382 + * @return array $args The expanded hook parameters.
383 + */
384 + public function expand_plugin_data( $args ) {
385 + $plugin_path = $args[0];
386 + $plugin_data = array();
387 +
388 + if ( ! function_exists( 'get_plugins' ) ) {
389 + require_once ABSPATH . 'wp-admin/includes/plugin.php';
390 + }
391 + $all_plugins = get_plugins();
392 + if ( isset( $all_plugins[ $plugin_path ] ) ) {
393 + $all_plugin_data = $all_plugins[ $plugin_path ];
394 + $plugin_data['name'] = $all_plugin_data['Name'];
395 + $plugin_data['version'] = $all_plugin_data['Version'];
396 + }
397 +
398 + return array(
399 + $args[0],
400 + $args[1],
401 + $plugin_data,
402 + );
403 + }
404 +
405 + /**
406 + * Helper method for firing the 'jetpack_plugins_updated' action on shutdown.
407 + *
408 + * @access public
409 + */
410 + public function sync_plugins_updated() {
411 + /**
412 + * Sync that a plugin update
413 + *
414 + * @since 1.6.3
415 + * @since-jetpack 5.8.0
416 + *
417 + * @module sync
418 + *
419 + * @param array () $plugin, Plugin Data
420 + */
421 + do_action( 'jetpack_plugins_updated', $this->plugins_updated, $this->state );
422 + }
423 +
424 + /**
425 + * Helper method for firing the 'jetpack_plugin_installed' action on shutdown.
426 + *
427 + * @access public
428 + */
429 + public function sync_plugins_installed() {
430 + if ( empty( $this->plugins_installed ) ) {
431 + return;
432 + }
433 + /**
434 + * Signals to the sync listener that a plugin was installed and a sync action
435 + * reflecting the installation and the plugin info should be sent.
436 + *
437 + * @since 1.6.3
438 + * @since-jetpack 5.8.0
439 + *
440 + * @module sync
441 + *
442 + * @param array () $plugin, Plugin Data
443 + */
444 + do_action( 'jetpack_plugin_installed', $this->plugins_installed );
445 + }
446 +
447 + /**
448 + * Helper method for firing the 'jetpack_plugin_update_failed' actions on shutdown.
449 + *
450 + * @access public
451 + */
452 + public function sync_plugins_update_failed() {
453 + if ( empty( $this->plugins_update_failures ) ) {
454 + return;
455 + }
456 + foreach ( $this->plugins_update_failures as $failure ) {
457 + /**
458 + * Sync that a plugin update failed
459 + *
460 + * @since 1.6.3
461 + * @since-jetpack 5.8.0
462 + *
463 + * @module sync
464 + *
465 + * @param array $plugin Plugin Data
466 + * @param string $code Error code
467 + * @param string $message Error message
468 + * @param array $state State data
469 + */
470 + do_action( 'jetpack_plugin_update_failed', $failure['plugin'], $failure['code'], $failure['message'], $failure['state'] );
471 + }
472 + }
473 +}