PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.5
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.5
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-sync / src / modules / class-plugins.php

class-plugins.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.5, at jetpack_vendor/automattic/jetpack-sync/src/modules/class-plugins.php

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