Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 286
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_JSON_API_Plugins_Modify_Endpoint
0.00% covered (danger)
0.00%
0 / 181
0.00% covered (danger)
0.00%
0 / 12
6480
0.00% covered (danger)
0.00%
0 / 1
 callback
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
72
 default_action
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
132
 autoupdate_on
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 autoupdate_off
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 autoupdate_translations_on
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 autoupdate_translations_off
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 activate
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
272
 current_user_can
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 deactivate
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
110
 update
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
420
 update_translations
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 get_translation
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3use Automattic\Jetpack\Constants;
4
5if ( ! defined( 'ABSPATH' ) ) {
6    exit( 0 );
7}
8
9new Jetpack_JSON_API_Plugins_Modify_Endpoint(
10    array(
11        'description'             => 'Activate/Deactivate a Plugin on your Jetpack Site, or set automatic updates',
12        'min_version'             => '1',
13        'max_version'             => '1.1',
14        'method'                  => 'POST',
15        'path'                    => '/sites/%s/plugins/%s',
16        'stat'                    => 'plugins:1:modify',
17        'path_labels'             => array(
18            '$site'   => '(int|string) The site ID, The site domain',
19            '$plugin' => '(string) The plugin ID',
20        ),
21        'allow_jetpack_site_auth' => true,
22        'request_format'          => array(
23            'action'           => '(string) Possible values are \'update\'',
24            'autoupdate'       => '(bool) Whether or not to automatically update the plugin',
25            'active'           => '(bool) Activate or deactivate the plugin',
26            'network_wide'     => '(bool) Do action network wide (default value: false)',
27            'scheduled_update' => '(bool) If the update is happening as a result of a scheduled update event',
28        ),
29        'query_parameters'        => array(
30            'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event',
31        ),
32        'response_format'         => Jetpack_JSON_API_Plugins_Endpoint::$_response_format,
33        'example_request_data'    => array(
34            'headers' => array(
35                'authorization' => 'Bearer YOUR_API_TOKEN',
36            ),
37            'body'    => array(
38                'action' => 'update',
39            ),
40        ),
41        'example_request'         => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/hello-dolly%20hello',
42    )
43);
44
45new Jetpack_JSON_API_Plugins_Modify_Endpoint(
46    array(
47        'description'             => 'Activate/Deactivate a list of plugins on your Jetpack Site, or set automatic updates',
48        'min_version'             => '1',
49        'max_version'             => '1.1',
50        'method'                  => 'POST',
51        'path'                    => '/sites/%s/plugins',
52        'stat'                    => 'plugins:modify',
53        'path_labels'             => array(
54            '$site' => '(int|string) The site ID, The site domain',
55        ),
56        'request_format'          => array(
57            'action'       => '(string) Possible values are \'update\'',
58            'autoupdate'   => '(bool) Whether or not to automatically update the plugin',
59            'active'       => '(bool) Activate or deactivate the plugin',
60            'network_wide' => '(bool) Do action network wide (default value: false)',
61            'plugins'      => '(array) A list of plugin ids to modify',
62        ),
63        'allow_jetpack_site_auth' => true,
64        'query_parameters'        => array(
65            'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event',
66        ),
67        'response_format'         => array(
68            'plugins'     => '(array:plugin) An array of plugin objects.',
69            'updated'     => '(array) A list of plugin ids that were updated. Only present if action is update.',
70            'not_updated' => '(array) A list of plugin ids that were not updated. Only present if action is update.',
71            'log'         => '(array) Update log. Only present if action is update.',
72        ),
73        'example_request_data'    => array(
74            'headers' => array(
75                'authorization' => 'Bearer YOUR_API_TOKEN',
76            ),
77            'body'    => array(
78                'active'  => true,
79                'plugins' => array(
80                    'jetpack/jetpack',
81                    'akismet/akismet',
82                ),
83            ),
84        ),
85        'example_request'         => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins',
86    )
87);
88
89new Jetpack_JSON_API_Plugins_Modify_Endpoint(
90    array(
91        'description'             => 'Update a Plugin on your Jetpack Site',
92        'min_version'             => '1',
93        'max_version'             => '1.1',
94        'method'                  => 'POST',
95        'path'                    => '/sites/%s/plugins/%s/update/',
96        'stat'                    => 'plugins:1:update',
97        'path_labels'             => array(
98            '$site'   => '(int|string) The site ID, The site domain',
99            '$plugin' => '(string) The plugin ID',
100        ),
101        'allow_jetpack_site_auth' => true,
102        'query_parameters'        => array(
103            'autoupdate' => '(bool=false) If the update is happening as a result of autoupdate event',
104        ),
105        'response_format'         => Jetpack_JSON_API_Plugins_Endpoint::$_response_format,
106        'example_request_data'    => array(
107            'headers' => array(
108                'authorization' => 'Bearer YOUR_API_TOKEN',
109            ),
110        ),
111        'example_request'         => 'https://public-api.wordpress.com/rest/v1/sites/example.wordpress.org/plugins/hello-dolly%20hello/update',
112    )
113);
114
115/**
116 * Plugins modify endpoint class.
117 *
118 * POST  /sites/%s/plugins/%s
119 * POST  /sites/%s/plugins
120 *
121 * @phan-constructor-used-for-side-effects
122 */
123class Jetpack_JSON_API_Plugins_Modify_Endpoint extends Jetpack_JSON_API_Plugins_Endpoint {
124
125    /**
126     * The slug.
127     *
128     * @var string
129     */
130    protected $slug = null;
131
132    /**
133     * Needed capabilities.
134     *
135     * @var string
136     */
137    protected $needed_capabilities = 'activate_plugins';
138
139    /**
140     * Action.
141     *
142     * @var string
143     */
144    protected $action = 'default_action';
145
146    /**
147     * Expected actions.
148     *
149     * @var array
150     */
151    protected $expected_actions = array( 'update', 'install', 'delete', 'update_translations' );
152
153    /**
154     * Callback.
155     *
156     * @param string $path - the path.
157     * @param int    $blog_id - the blog ID.
158     * @param object $object - the object.
159     *
160     * @return bool|WP_Error
161     */
162    public function callback( $path = '', $blog_id = 0, $object = null ) {
163
164        Jetpack_JSON_API_Endpoint::validate_input( $object );
165        switch ( $this->action ) {
166            case 'delete':
167                $this->needed_capabilities = 'delete_plugins';
168                break;
169            case 'update_translations':
170            case 'update':
171                $this->needed_capabilities = 'update_plugins';
172                break;
173            case 'install':
174                $this->needed_capabilities = 'install_plugins';
175                break;
176        }
177
178        $args = $this->input();
179
180        if ( is_array( $args ) && ( isset( $args['autoupdate'] ) || isset( $args['autoupdate_translations'] ) ) ) {
181            $this->needed_capabilities = 'update_plugins';
182        }
183
184        return parent::callback( $path, $blog_id, $object );
185    }
186
187    /**
188     * The default action.
189     *
190     * @return bool|WP_Error
191     */
192    public function default_action() {
193        $args = $this->input();
194
195        if ( isset( $args['autoupdate'] ) && is_bool( $args['autoupdate'] ) ) {
196            if ( $args['autoupdate'] ) {
197                $this->autoupdate_on();
198            } else {
199                $this->autoupdate_off();
200            }
201        }
202
203        if ( isset( $args['active'] ) && is_bool( $args['active'] ) ) {
204            if ( $args['active'] ) {
205                // We don't have to check for activate_plugins permissions since we assume that the user has those
206                // Since we set them via $needed_capabilities.
207                return $this->activate();
208            } elseif ( $this->current_user_can( 'deactivate_plugins' ) ) {
209                return $this->deactivate();
210            } else {
211                return new WP_Error( 'unauthorized_error', __( 'Plugin deactivation is not allowed', 'jetpack' ), '403' );
212            }
213        }
214
215        if ( isset( $args['autoupdate_translations'] ) && is_bool( $args['autoupdate_translations'] ) ) {
216            if ( $args['autoupdate_translations'] ) {
217                $this->autoupdate_translations_on();
218            } else {
219                $this->autoupdate_translations_off();
220            }
221        }
222
223        return true;
224    }
225
226    /**
227     * Turn on autoupdate.
228     */
229    protected function autoupdate_on() {
230        $autoupdate_plugins = (array) get_site_option( 'auto_update_plugins', array() );
231        $autoupdate_plugins = array_unique( array_merge( $autoupdate_plugins, $this->plugins ) );
232        update_site_option( 'auto_update_plugins', $autoupdate_plugins );
233    }
234
235    /**
236     * Turn off autoupdate.
237     */
238    protected function autoupdate_off() {
239        $autoupdate_plugins = (array) get_site_option( 'auto_update_plugins', array() );
240        $autoupdate_plugins = array_diff( $autoupdate_plugins, $this->plugins );
241        update_site_option( 'auto_update_plugins', $autoupdate_plugins );
242    }
243
244    /**
245     * Turn autoupdate translations on.
246     */
247    protected function autoupdate_translations_on() {
248        $autoupdate_plugins = Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() );
249        $autoupdate_plugins = array_unique( array_merge( $autoupdate_plugins, $this->plugins ) );
250        Jetpack_Options::update_option( 'autoupdate_plugins_translations', $autoupdate_plugins );
251    }
252
253    /**
254     * Turn autoupdate translations off.
255     */
256    protected function autoupdate_translations_off() {
257        $autoupdate_plugins = Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() );
258        $autoupdate_plugins = array_diff( $autoupdate_plugins, $this->plugins );
259        Jetpack_Options::update_option( 'autoupdate_plugins_translations', $autoupdate_plugins );
260    }
261
262    /**
263     * Activate the plugin.
264     *
265     * @return null|WP_Error null if the activation was successful.
266     */
267    protected function activate() {
268        $permission_error = false;
269        foreach ( $this->plugins as $plugin ) {
270
271            if ( ! $this->current_user_can( 'activate_plugin', $plugin ) ) {
272                $this->log[ $plugin ]['error'] = __( 'Sorry, you are not allowed to activate this plugin.', 'jetpack' );
273                $has_errors                    = true;
274                $permission_error              = true;
275                continue;
276            }
277
278            if ( ( ! $this->network_wide && Jetpack::is_plugin_active( $plugin ) ) || is_plugin_active_for_network( $plugin ) ) {
279                $this->log[ $plugin ]['error'] = __( 'The Plugin is already active.', 'jetpack' );
280                $has_errors                    = true;
281                continue;
282            }
283
284            if ( ! $this->network_wide && is_network_only_plugin( $plugin ) && is_multisite() ) {
285                $this->log[ $plugin ]['error'] = __( 'Plugin can only be Network Activated', 'jetpack' );
286                $has_errors                    = true;
287                continue;
288            }
289
290            $result = activate_plugin( $plugin, '', $this->network_wide );
291
292            if ( is_wp_error( $result ) ) {
293                $this->log[ $plugin ]['error'] = $result->get_error_messages();
294                $has_errors                    = true;
295                continue;
296            }
297
298            $success = Jetpack::is_plugin_active( $plugin );
299            if ( $success && $this->network_wide ) {
300                $success &= is_plugin_active_for_network( $plugin );
301            }
302
303            if ( ! $success ) {
304                $this->log[ $plugin ]['error'] = $result->get_error_messages;
305                $has_errors                    = true;
306                continue;
307            }
308            $this->log[ $plugin ][] = __( 'Plugin activated.', 'jetpack' );
309        }
310
311        if ( ! $this->bulk && isset( $has_errors ) ) {
312            $plugin = $this->plugins[0];
313            if ( $permission_error ) {
314                return new WP_Error( 'unauthorized_error', $this->log[ $plugin ]['error'], 403 );
315            }
316
317            return new WP_Error( 'activation_error', $this->log[ $plugin ]['error'] );
318        }
319    }
320
321    /**
322     * Check if the current user has capabilities.
323     *
324     * @param string $capability - the capability we're checking.
325     * @param string $plugin - the plugin we're checking.
326     *
327     * @return bool
328     */
329    protected function current_user_can( $capability, $plugin = null ) {
330        // If this endpoint accepts site based authentication and a blog token is used, skip capabilities check.
331        if ( $this->accepts_site_based_authentication() ) {
332            return true;
333        }
334        if ( $plugin ) {
335            return current_user_can( $capability, $plugin );
336        }
337
338        return current_user_can( $capability );
339    }
340
341    /**
342     * Deactivate the plugin.
343     *
344     * @return null|WP_Error null if the deactivation was successful
345     */
346    protected function deactivate() {
347        $permission_error = false;
348        foreach ( $this->plugins as $plugin ) {
349            if ( ! $this->current_user_can( 'deactivate_plugin', $plugin ) ) {
350                $error                         = __( 'Sorry, you are not allowed to deactivate this plugin.', 'jetpack' );
351                $this->log[ $plugin ]['error'] = $error;
352                $permission_error              = true;
353                continue;
354            }
355
356            if ( ! Jetpack::is_plugin_active( $plugin ) ) {
357                $error                         = __( 'The Plugin is already deactivated.', 'jetpack' );
358                $this->log[ $plugin ]['error'] = $error;
359                continue;
360            }
361
362            deactivate_plugins( $plugin, false, $this->network_wide );
363
364            $success = ! Jetpack::is_plugin_active( $plugin );
365            if ( $success && $this->network_wide ) {
366                $success &= ! is_plugin_active_for_network( $plugin );
367            }
368
369            if ( ! $success ) {
370                $error                         = __( 'There was an error deactivating your plugin', 'jetpack' );
371                $this->log[ $plugin ]['error'] = $error;
372                continue;
373            }
374            $this->log[ $plugin ][] = __( 'Plugin deactivated.', 'jetpack' );
375        }
376        if ( ! $this->bulk && isset( $error ) ) {
377            if ( $permission_error ) {
378                return new WP_Error( 'unauthorized_error', $error, 403 );
379            }
380
381            return new WP_Error( 'deactivation_error', $error );
382        }
383    }
384
385    /**
386     * Update the plugin.
387     *
388     * @return bool|WP_Error
389     */
390    protected function update() {
391        $query_args = $this->query_args();
392
393        $is_automatic_update = false;
394        if ( isset( $query_args['autoupdate'] ) && $query_args['autoupdate'] || $this->scheduled_update ) {
395            $is_automatic_update = true;
396        }
397
398        if ( $this->scheduled_update ) {
399            Constants::set_constant( 'SCHEDULED_AUTOUPDATE', true );
400        }
401        wp_clean_plugins_cache( false );
402        ob_start();
403        wp_update_plugins(); // Check for Plugin updates
404        ob_end_clean();
405
406        $update_plugins = get_site_transient( 'update_plugins' );
407
408        if ( isset( $update_plugins->response ) ) {
409            $plugin_updates_needed = array_keys( $update_plugins->response );
410        } else {
411            $plugin_updates_needed = array();
412        }
413
414        $update_attempted = false;
415
416        include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
417
418        // unhook this functions that output things before we send our response header.
419        remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 );
420        remove_action( 'upgrader_process_complete', 'wp_version_check' );
421        remove_action( 'upgrader_process_complete', 'wp_update_themes' );
422
423        // Set the lock timeout to 15 minutes if it's scheduled update, otherwise default to one hour.
424        $lock_release_timeout = $this->scheduled_update ? 15 * MINUTE_IN_SECONDS : null;
425
426        // Early return if unable to obtain auto_updater lock.
427        // @see https://github.com/WordPress/wordpress-develop/blob/66469efa99e7978c8824e287834135aa9842e84f/src/wp-admin/includes/class-wp-automatic-updater.php#L453.
428        if ( $is_automatic_update && ! WP_Upgrader::create_lock( 'auto_updater', $lock_release_timeout ) ) {
429            return new WP_Error( 'update_fail', __( 'Updates are already in progress.', 'jetpack' ), 400 );
430        }
431
432        $result = false;
433
434        foreach ( $this->plugins as $plugin ) {
435
436            if ( ! in_array( $plugin, $plugin_updates_needed, true ) ) {
437                $this->log[ $plugin ][] = __( 'No update needed', 'jetpack' );
438                continue;
439            }
440
441            // Rely on WP_Automatic_Updater class to check if a plugin item should be updated if it is a Jetpack autoupdate request.
442            if ( $is_automatic_update && ! ( new WP_Automatic_Updater() )->should_update( 'plugin', $update_plugins->response[ $plugin ], WP_PLUGIN_DIR ) ) {
443                continue;
444            }
445
446            // Establish per plugin lock.
447            $plugin_slug = Jetpack_Autoupdate::get_plugin_slug( $plugin );
448            if ( ! WP_Upgrader::create_lock( 'jetpack_' . $plugin_slug, $lock_release_timeout ) ) {
449                continue;
450            }
451
452            /**
453             * Pre-upgrade action
454             *
455             * @since 3.9.3
456             *
457             * @param array $plugin            Plugin data
458             * @param array $plugin            Array of plugin objects
459             * @param bool  $updated_attempted false for the first update, true subsequently
460             */
461            do_action( 'jetpack_pre_plugin_upgrade', $plugin, $this->plugins, $update_attempted );
462
463            $update_attempted = true;
464
465            // Object created inside the for loop to clean the messages for each plugin
466            $skin = new WP_Ajax_Upgrader_Skin();
467            // The Automatic_Upgrader_Skin skin shouldn't output anything.
468            $upgrader = new Plugin_Upgrader( $skin );
469            $upgrader->init();
470            // This avoids the plugin to be deactivated.
471            // Using bulk upgrade puts the site into maintenance mode during the upgrades
472            $result               = $upgrader->bulk_upgrade( array( $plugin ) );
473            $errors               = $skin->get_errors();
474            $this->log[ $plugin ] = $skin->get_upgrade_messages();
475
476            // release individual plugin lock.
477            WP_Upgrader::release_lock( 'jetpack_' . $plugin_slug );
478
479            if ( is_wp_error( $errors ) && $errors->get_error_code() ) {
480                return $errors;
481            }
482        }
483
484        // release auto_udpate lock.
485        if ( $is_automatic_update ) {
486            WP_Upgrader::release_lock( 'auto_updater' );
487        }
488
489        if ( ! $this->bulk && ! $result && $update_attempted ) {
490            return new WP_Error( 'update_fail', __( 'There was an error updating your plugin', 'jetpack' ), 400 );
491        }
492
493        return $this->default_action();
494    }
495
496    /**
497     * Update translations.
498     *
499     * @return bool|WP_Error
500     */
501    public function update_translations() {
502        include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
503
504        // Clear the cache.
505        wp_clean_plugins_cache( false );
506        ob_start();
507        wp_update_plugins(); // Check for Plugin updates
508        ob_end_clean();
509
510        $available_updates = get_site_transient( 'update_plugins' );
511        if ( ! isset( $available_updates->translations ) || empty( $available_updates->translations ) ) {
512            return new WP_Error( 'nothing_to_translate' );
513        }
514
515        $update_attempted = false;
516        $result           = false;
517        foreach ( $this->plugins as $plugin ) {
518            $this->slug  = Jetpack_Autoupdate::get_plugin_slug( $plugin );
519            $translation = array_filter( $available_updates->translations, array( $this, 'get_translation' ) );
520
521            if ( empty( $translation ) ) {
522                $this->log[ $plugin ][] = __( 'No update needed', 'jetpack' );
523                continue;
524            }
525
526            /**
527             * Pre-upgrade action
528             *
529             * @since 4.4.0
530             *
531             * @param array $plugin           Plugin data
532             * @param array $plugin           Array of plugin objects
533             * @param bool  $update_attempted false for the first update, true subsequently
534             */
535            do_action( 'jetpack_pre_plugin_upgrade_translations', $plugin, $this->plugins, $update_attempted );
536
537            $update_attempted = true;
538
539            $skin     = new Automatic_Upgrader_Skin();
540            $upgrader = new Language_Pack_Upgrader( $skin );
541            $upgrader->init();
542
543            $result = $upgrader->upgrade( (object) $translation[0] );
544
545            $this->log[ $plugin ] = $upgrader->skin->get_upgrade_messages();
546        }
547
548        if ( ! $this->bulk && ! $result ) {
549            return new WP_Error( 'update_fail', __( 'There was an error updating your plugin', 'jetpack' ), 400 );
550        }
551
552        return true;
553    }
554
555    /**
556     * Test whether the translation matches `$this->slug`.
557     *
558     * @param array $translation - the translation.
559     *
560     * @return bool
561     */
562    protected function get_translation( $translation ) {
563        return ( $translation['slug'] === $this->slug );
564    }
565}