Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 182
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_JSON_API_Plugins_Endpoint
0.00% covered (danger)
0.00%
0 / 180
0.00% covered (danger)
0.00%
0 / 13
6320
0.00% covered (danger)
0.00%
0 / 1
 result
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 validate_input
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
110
 validate_plugins
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 format_plugin
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
72
 format_plugin_v1_2
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
56
 plugin_has_translations_autoupdates_enabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_file_mod_capabilities
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
110
 get_plugins
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
90
 validate_network_wide
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
42
 validate_plugin
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 validate_scheduled_update
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 get_plugin_updates
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
110
 get_plugin_action_links
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;
4use Automattic\Jetpack\Current_Plan;
5use Automattic\Jetpack\Sync\Functions;
6
7if ( ! defined( 'ABSPATH' ) ) {
8    exit( 0 );
9}
10
11/**
12 * Base class for working with plugins.
13 */
14abstract class Jetpack_JSON_API_Plugins_Endpoint extends Jetpack_JSON_API_Endpoint {
15
16    /**
17     * Plugins.
18     *
19     * @var array
20     */
21    protected $plugins = array();
22
23    /**
24     * If the plugin is network wide.
25     *
26     * @var boolean
27     */
28    protected $network_wide = false;
29
30    /**
31     * If we're working in bulk.
32     *
33     * @var boolean
34     */
35    protected $bulk = true;
36
37    /**
38     * The log.
39     *
40     * @var array
41     */
42    protected $log;
43
44    /**
45     * If the request is a scheduled update.
46     *
47     * @var boolean
48     */
49    protected $scheduled_update = false;
50
51    /**
52     * Response format.
53     *
54     * @var array
55     */
56    public static $_response_format = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
57        'id'                     => '(safehtml)  The plugin\'s ID',
58        'slug'                   => '(safehtml)  The plugin\'s .org slug',
59        'active'                 => '(boolean) The plugin status.',
60        'update'                 => '(object)  The plugin update info.',
61        'name'                   => '(safehtml)  The name of the plugin.',
62        'plugin_url'             => '(url)  Link to the plugin\'s web site.',
63        'version'                => '(safehtml)  The plugin version number.',
64        'description'            => '(safehtml)  Description of what the plugin does and/or notes from the author',
65        'author'                 => '(safehtml)  The author\'s name',
66        'author_url'             => '(url)  The authors web site address',
67        'network'                => '(boolean) Whether the plugin can only be activated network wide.',
68        'autoupdate'             => '(boolean) Whether the plugin is automatically updated',
69        'autoupdate_translation' => '(boolean) Whether the plugin is automatically updating translations',
70        'next_autoupdate'        => '(string) Y-m-d H:i:s for next scheduled update event',
71        'log'                    => '(array:safehtml) An array of update log strings.',
72        'uninstallable'          => '(boolean) Whether the plugin is unistallable.',
73        'action_links'           => '(array) An array of action links that the plugin uses.',
74    );
75
76    /**
77     * Response format v1_2
78     *
79     * @var array
80     */
81    public static $_response_format_v1_2 = array( // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
82        'slug'                   => '(safehtml) The plugin\'s .org slug',
83        'active'                 => '(boolean) The plugin status.',
84        'update'                 => '(object) The plugin update info.',
85        'name'                   => '(safehtml) The plugin\'s ID',
86        'display_name'           => '(safehtml) The name of the plugin.',
87        'version'                => '(safehtml) The plugin version number.',
88        'description'            => '(safehtml) Description of what the plugin does and/or notes from the author',
89        'author'                 => '(safehtml) The author\'s name',
90        'author_url'             => '(url) The authors web site address',
91        'plugin_url'             => '(url) Link to the plugin\'s web site.',
92        'network'                => '(boolean) Whether the plugin can only be activated network wide.',
93        'autoupdate'             => '(boolean) Whether the plugin is automatically updated',
94        'autoupdate_translation' => '(boolean) Whether the plugin is automatically updating translations',
95        'uninstallable'          => '(boolean) Whether the plugin is unistallable.',
96        'action_links'           => '(array) An array of action links that the plugin uses.',
97        'log'                    => '(array:safehtml) An array of update log strings.',
98    );
99
100    /**
101     * The result.
102     *
103     * @return array
104     */
105    protected function result() {
106
107        $plugins = $this->get_plugins();
108
109        if ( ! $this->bulk && ! empty( $plugins ) ) {
110            return array_pop( $plugins );
111        }
112
113        return array( 'plugins' => $plugins );
114    }
115
116    /**
117     * Validate the input.
118     *
119     * @param string $plugin - the plugin we're validating.
120     *
121     * @return bool|WP_Error
122     */
123    protected function validate_input( $plugin ) {
124
125        $error = parent::validate_input( $plugin );
126        if ( is_wp_error( $error ) ) {
127            return $error;
128        }
129
130        $error = $this->validate_network_wide();
131        if ( is_wp_error( $error ) ) {
132            return $error;
133        }
134
135        $error = $this->validate_scheduled_update();
136        if ( is_wp_error( $error ) ) {
137            return $error;
138        }
139
140        $args = $this->input();
141        // find out what plugin, or plugins we are dealing with
142        // validate the requested plugins
143        if ( ! isset( $plugin ) || empty( $plugin ) ) {
144            if ( ! $args['plugins'] || empty( $args['plugins'] ) ) {
145                return new WP_Error( 'missing_plugin', __( 'You are required to specify a plugin.', 'jetpack' ), 400 );
146            }
147            if ( is_array( $args['plugins'] ) ) {
148                $this->plugins = $args['plugins'];
149            } else {
150                $this->plugins[] = $args['plugins'];
151            }
152        } else {
153            $this->bulk      = false;
154            $this->plugins[] = urldecode( $plugin );
155        }
156
157        $error = $this->validate_plugins();
158        if ( is_wp_error( $error ) ) {
159            return $error;
160        }
161
162        return true;
163    }
164
165    /**
166     * Walks through submitted plugins to make sure they are valid
167     *
168     * @return bool|WP_Error
169     */
170    protected function validate_plugins() {
171        if ( empty( $this->plugins ) || ! is_array( $this->plugins ) ) {
172            return new WP_Error( 'missing_plugins', __( 'No plugins found.', 'jetpack' ) );
173        }
174        foreach ( $this->plugins as $index => $plugin ) {
175            if ( ! preg_match( '/\.php$/', $plugin ) ) {
176                $plugin                  = $plugin . '.php';
177                $this->plugins[ $index ] = $plugin;
178            }
179            $valid = $this->validate_plugin( urldecode( $plugin ) );
180            if ( is_wp_error( $valid ) ) {
181                return $valid;
182            }
183        }
184
185        return true;
186    }
187
188    /**
189     * Format the plugin.
190     *
191     * @param string $plugin_file - the plugin file.
192     * @param array  $plugin_data - the plugin data.
193     *
194     * @return array
195     */
196    protected function format_plugin( $plugin_file, $plugin_data ) {
197        if ( version_compare( $this->min_version, '1.2', '>=' ) ) {
198            return $this->format_plugin_v1_2( $plugin_file, $plugin_data );
199        }
200        $plugin                    = array();
201        $plugin['id']              = preg_replace( '/(.+)\.php$/', '$1', $plugin_file );
202        $plugin['slug']            = Jetpack_Autoupdate::get_plugin_slug( $plugin_file );
203        $plugin['active']          = Jetpack::is_plugin_active( $plugin_file );
204        $plugin['name']            = $plugin_data['Name'];
205        $plugin['plugin_url']      = $plugin_data['PluginURI'];
206        $plugin['version']         = $plugin_data['Version'];
207        $plugin['description']     = $plugin_data['Description'];
208        $plugin['author']          = $plugin_data['Author'];
209        $plugin['author_url']      = $plugin_data['AuthorURI'];
210        $plugin['network']         = $plugin_data['Network'];
211        $plugin['update']          = $this->get_plugin_updates( $plugin_file );
212        $plugin['next_autoupdate'] = gmdate( 'Y-m-d H:i:s', wp_next_scheduled( 'wp_maybe_auto_update' ) );
213        $action_link               = $this->get_plugin_action_links( $plugin_file );
214        if ( ! empty( $action_link ) ) {
215            $plugin['action_links'] = $action_link;
216        }
217
218        $plugin['plugin'] = $plugin_file;
219        if ( ! class_exists( 'WP_Automatic_Updater' ) ) {
220            require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
221        }
222        $autoupdate           = ( new WP_Automatic_Updater() )->should_update( 'plugin', (object) $plugin, WP_PLUGIN_DIR );
223        $plugin['autoupdate'] = $autoupdate;
224
225        $autoupdate_translation           = in_array( $plugin_file, Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() ), true );
226        $plugin['autoupdate_translation'] = $autoupdate || $autoupdate_translation || Jetpack_Options::get_option( 'autoupdate_translations', false );
227
228        $plugin['uninstallable'] = is_uninstallable_plugin( $plugin_file );
229
230        if ( is_multisite() ) {
231            $plugin['network_active'] = is_plugin_active_for_network( $plugin_file );
232        }
233
234        if ( ! empty( $this->log[ $plugin_file ] ) ) {
235            $plugin['log'] = $this->log[ $plugin_file ];
236        }
237        return $plugin;
238    }
239
240    /**
241     * Format the plugin for v1_2.
242     *
243     * @param string $plugin_file - the plugin file.
244     * @param array  $plugin_data - the plugin data.
245     *
246     * @return array
247     */
248    protected function format_plugin_v1_2( $plugin_file, $plugin_data ) {
249        $plugin                 = array();
250        $plugin['slug']         = Jetpack_Autoupdate::get_plugin_slug( $plugin_file );
251        $plugin['active']       = Jetpack::is_plugin_active( $plugin_file );
252        $plugin['name']         = preg_replace( '/(.+)\.php$/', '$1', $plugin_file );
253        $plugin['display_name'] = $plugin_data['Name'];
254        $plugin['plugin_url']   = $plugin_data['PluginURI'];
255        $plugin['version']      = $plugin_data['Version'];
256        $plugin['description']  = $plugin_data['Description'];
257        $plugin['author']       = $plugin_data['Author'];
258        $plugin['author_url']   = $plugin_data['AuthorURI'];
259        $plugin['network']      = $plugin_data['Network'];
260        $plugin['update']       = $this->get_plugin_updates( $plugin_file );
261        $action_link            = $this->get_plugin_action_links( $plugin_file );
262        if ( ! empty( $action_link ) ) {
263            $plugin['action_links'] = $action_link;
264        }
265
266        $plugin['plugin'] = $plugin_file;
267        if ( ! class_exists( 'WP_Automatic_Updater' ) ) {
268            require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
269        }
270        $autoupdate           = ( new WP_Automatic_Updater() )->should_update( 'plugin', (object) $plugin, WP_PLUGIN_DIR );
271        $plugin['autoupdate'] = $autoupdate;
272
273        $autoupdate_translation           = $this->plugin_has_translations_autoupdates_enabled( $plugin_file );
274        $plugin['autoupdate_translation'] = $autoupdate || $autoupdate_translation || Jetpack_Options::get_option( 'autoupdate_translations', false );
275        $plugin['uninstallable']          = is_uninstallable_plugin( $plugin_file );
276
277        if ( is_multisite() ) {
278            $plugin['network_active'] = is_plugin_active_for_network( $plugin_file );
279        }
280
281        if ( ! empty( $this->log[ $plugin_file ] ) ) {
282            $plugin['log'] = $this->log[ $plugin_file ];
283        }
284
285        return $plugin;
286    }
287
288    /**
289     * Check if plugin has autoupdates for translations enabled.
290     *
291     * @param string $plugin_file - the plugin file.
292     *
293     * @return bool
294     */
295    protected function plugin_has_translations_autoupdates_enabled( $plugin_file ) {
296        return in_array( $plugin_file, Jetpack_Options::get_option( 'autoupdate_plugins_translations', array() ), true );
297    }
298
299    /**
300     * Get file mod capabilities.
301     */
302    protected function get_file_mod_capabilities() {
303        $reasons_can_not_autoupdate   = array();
304        $reasons_can_not_modify_files = array();
305
306        $has_file_system_write_access = Functions::file_system_write_access();
307        if ( ! $has_file_system_write_access ) {
308            $reasons_can_not_modify_files['has_no_file_system_write_access'] = __( 'The file permissions on this host prevent editing files.', 'jetpack' );
309        }
310
311        $disallow_file_mods = Constants::get_constant( 'DISALLOW_FILE_MODS' );
312        if ( $disallow_file_mods ) {
313            $reasons_can_not_modify_files['disallow_file_mods'] = __( 'File modifications are explicitly disabled by a site administrator.', 'jetpack' );
314        }
315
316        $automatic_updater_disabled = Constants::get_constant( 'AUTOMATIC_UPDATER_DISABLED' );
317        if ( $automatic_updater_disabled ) {
318            $reasons_can_not_autoupdate['automatic_updater_disabled'] = __( 'Any autoupdates are explicitly disabled by a site administrator.', 'jetpack' );
319        }
320
321        if ( is_multisite() ) {
322            // is it the main network ? is really is multi network
323            if ( Jetpack::is_multi_network() ) {
324                $reasons_can_not_modify_files['is_multi_network'] = __( 'Multi network install are not supported.', 'jetpack' );
325            }
326            // Is the site the main site here.
327            if ( ! is_main_site() ) {
328                $reasons_can_not_modify_files['is_sub_site'] = __( 'The site is not the main network site', 'jetpack' );
329            }
330        }
331
332        $file_mod_capabilities = array(
333            'modify_files'     => empty( $reasons_can_not_modify_files ), // install, remove, update
334            'autoupdate_files' => empty( $reasons_can_not_modify_files ) && empty( $reasons_can_not_autoupdate ), // enable autoupdates
335        );
336
337        if ( ! empty( $reasons_can_not_modify_files ) ) {
338            $file_mod_capabilities['reasons_modify_files_unavailable'] = $reasons_can_not_modify_files;
339        }
340
341        if ( ! $file_mod_capabilities['autoupdate_files'] ) {
342            $file_mod_capabilities['reasons_autoupdate_unavailable'] = array_merge( $reasons_can_not_autoupdate, $reasons_can_not_modify_files );
343        }
344        return $file_mod_capabilities;
345    }
346
347    /**
348     * Get plugins.
349     *
350     * @return array
351     */
352    protected function get_plugins() {
353        $plugins = array();
354        /** This filter is documented in wp-admin/includes/class-wp-plugins-list-table.php */
355        $installed_plugins = apply_filters( 'all_plugins', get_plugins() );
356        foreach ( $this->plugins as $plugin ) {
357            if ( ! isset( $installed_plugins[ $plugin ] ) ) {
358                continue;
359            }
360
361            $formatted_plugin = $this->format_plugin( $plugin, $installed_plugins[ $plugin ] );
362
363            // If this endpoint accepts site based authentication and a blog token is used, skip capabilities check.
364            if ( $this->accepts_site_based_authentication() ) {
365                $plugins[] = $formatted_plugin;
366                continue;
367            }
368
369            /*
370             * Do not show network-active plugins
371             * to folks who do not have the permission to see them.
372             */
373            if (
374                /** This filter is documented in src/wp-admin/includes/class-wp-plugins-list-table.php */
375                ! apply_filters( 'show_network_active_plugins', current_user_can( 'manage_network_plugins' ) )
376                && ! empty( $formatted_plugin['network_active'] )
377                && true === $formatted_plugin['network_active']
378            ) {
379                continue;
380            }
381
382            $plugins[] = $formatted_plugin;
383        }
384        $args = $this->query_args();
385
386        if ( isset( $args['offset'] ) ) {
387            $plugins = array_slice( $plugins, (int) $args['offset'] );
388        }
389        if ( isset( $args['limit'] ) ) {
390            $plugins = array_slice( $plugins, 0, (int) $args['limit'] );
391        }
392
393        return $plugins;
394    }
395
396    /**
397     * Validate network wide.
398     *
399     * @return bool|WP_Error
400     */
401    protected function validate_network_wide() {
402        $args = $this->input();
403
404        if ( isset( $args['network_wide'] ) && $args['network_wide'] ) {
405            $this->network_wide = true;
406        }
407
408        // If this endpoint accepts site based authentication and a blog token is used, skip capabilities check.
409        if ( $this->accepts_site_based_authentication() ) {
410            return true;
411        }
412
413        if ( $this->network_wide && ! current_user_can( 'manage_network_plugins' ) ) {
414            return new WP_Error( 'unauthorized', __( 'This user is not authorized to manage plugins network wide.', 'jetpack' ), 403 );
415        }
416
417        return true;
418    }
419
420    /**
421     * Validate the plugin.
422     *
423     * @param string $plugin - the plugin we're validating.
424     *
425     * @return bool|WP_Error
426     */
427    protected function validate_plugin( $plugin ) {
428        if ( ! isset( $plugin ) || empty( $plugin ) ) {
429            return new WP_Error( 'missing_plugin', __( 'You are required to specify a plugin to activate.', 'jetpack' ), 400 );
430        }
431
432        $error = validate_plugin( $plugin );
433        if ( is_wp_error( $error ) ) {
434            return new WP_Error( 'unknown_plugin', $error->get_error_messages(), 404 );
435        }
436
437        return true;
438    }
439
440    /**
441     * Validates if scheduled updates are allowed based on the current plan.
442     *
443     * @return bool|WP_Error True if scheduled updates are allowed or not provided, WP_Error otherwise.
444     */
445    protected function validate_scheduled_update() {
446        $args = $this->input();
447
448        if ( isset( $args['scheduled_update'] ) && $args['scheduled_update'] ) {
449            if ( Current_Plan::supports( 'scheduled-updates' ) ) {
450                $this->scheduled_update = true;
451            } else {
452                return new WP_Error( 'unauthorized', __( 'Scheduled updates are not available on your current plan. Please upgrade to a plan that supports scheduled updates to use this feature.', 'jetpack' ), 403 );
453            }
454        }
455
456        return true;
457    }
458
459    /**
460     * Get plugin updates.
461     *
462     * @param string $plugin_file - the plugin file.
463     *
464     * @return object|null
465     */
466    protected function get_plugin_updates( $plugin_file ) {
467        $plugin_updates = get_plugin_updates();
468        if ( isset( $plugin_updates[ $plugin_file ] ) ) {
469            $update         = $plugin_updates[ $plugin_file ]->update;
470            $cleaned_update = array();
471            foreach ( (array) $update as $update_key => $update_value ) {
472                switch ( $update_key ) {
473                    case 'id':
474                    case 'slug':
475                    case 'plugin':
476                    case 'new_version':
477                    case 'tested':
478                        $cleaned_update[ $update_key ] = wp_kses( $update_value, array() );
479                        break;
480                    case 'url':
481                    case 'package':
482                        $cleaned_update[ $update_key ] = esc_url( $update_value );
483                        break;
484                }
485            }
486            return (object) $cleaned_update;
487        }
488        return null;
489    }
490
491    /**
492     * Get plugin action links.
493     *
494     * @param string $plugin_file - the plugin file.
495     *
496     * @return array
497     */
498    protected function get_plugin_action_links( $plugin_file ) {
499        return Functions::get_plugins_action_links( $plugin_file );
500    }
501}