Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 312
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
PluginInstaller
0.00% covered (danger)
0.00%
0 / 312
0.00% covered (danger)
0.00%
0 / 18
9312
0.00% covered (danger)
0.00%
0 / 1
 install
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
420
 install_from_wordpress
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
20
 install_premium_plugin
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 1
342
 install_from_zip
0.00% covered (danger)
0.00%
0 / 65
0.00% covered (danger)
0.00%
0 / 1
210
 is_nfd_slug
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 is_plugin_installed
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 get_plugin_type
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 get_plugin_path
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 exists
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 is_active
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 activate
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 deactivate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 install_endurance_page_cache
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
30
 connect_to_filesystem
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 check_install_permissions
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 rest_get_plugin_install_hash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rest_verify_plugin_install_hash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_plugin_status
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2namespace NewfoldLabs\WP\Module\Installer\Services;
3
4use NewfoldLabs\WP\Module\Installer\Data\Plugins;
5use NewfoldLabs\WP\Module\Installer\Permissions;
6use NewfoldLabs\WP\Module\PLS\Utilities\PLSUtility;
7
8/**
9 * Class PluginInstaller
10 */
11class PluginInstaller {
12
13    /**
14     * Install a whitelisted plugin.
15     *
16     * @param string  $plugin The plugin slug from Plugins.php.
17     * @param boolean $should_activate Whether to activate the plugin after install.
18     *
19     * @return \WP_Error|\WP_REST_Response
20     */
21    public static function install( $plugin, $should_activate = true ) {
22        $plugins_list = Plugins::get();
23
24        // Check if the plugin param contains a zip url.
25        if ( \wp_http_validate_url( $plugin ) ) {
26            $domain = \wp_parse_url( $plugin, PHP_URL_HOST );
27            // If the zip URL/domain is not approved.
28            if ( ! isset( $plugins_list['urls'][ $plugin ] )
29                && ! isset( $plugins_list['domains'][ $domain ] ) ) {
30                    return new \WP_Error(
31                        'plugin-error',
32                        /* Translators: %s plugin slug */
33                        sprintf( __( 'You do not have permission to install from %s.', 'wp-module-installer' ), $plugin ),
34                        array( 'status' => 400 )
35                    );
36            }
37
38            $status = self::install_from_zip( $plugin, $should_activate );
39            if ( \is_wp_error( $status ) ) {
40                return $status;
41            }
42
43            return new \WP_REST_Response(
44                array(),
45                201
46            );
47        }
48
49        // If it is not a zip URL then check if it is an approved slug.
50        $plugin = \sanitize_text_field( $plugin );
51        if ( self::is_nfd_slug( $plugin ) ) {
52            // [TODO] Better handle mu-plugins and direct file downloads.
53            if ( 'nfd_slug_endurance_page_cache' === $plugin ) {
54                return self::install_endurance_page_cache();
55            }
56            $plugin_path = $plugins_list['nfd_slugs'][ $plugin ]['path'];
57            if ( ! self::is_plugin_installed( $plugin_path ) ) {
58                $status = self::install_from_zip( $plugins_list['nfd_slugs'][ $plugin ]['url'], $should_activate );
59                if ( \is_wp_error( $status ) ) {
60                    return $status;
61                }
62            }
63            if ( $should_activate && ! \is_plugin_active( $plugin_path ) ) {
64                $status = \activate_plugin( $plugin_path );
65                if ( \is_wp_error( $status ) ) {
66                    $status->add_data( array( 'status' => 500 ) );
67
68                    return $status;
69                }
70            }
71            return new \WP_REST_Response(
72                array(),
73                201
74            );
75        }
76
77        if ( ! isset( $plugins_list['wp_slugs'][ $plugin ] ) ) {
78            return new \WP_Error(
79                'plugin-error',
80                /* Translators: %s plugin slug */
81                sprintf( __( 'You do not have permission to install %s.', 'wp-module-installer' ), $plugin ),
82                array( 'status' => 400 )
83            );
84        }
85
86        $plugin_path                  = $plugins_list['wp_slugs'][ $plugin ]['path'];
87        $plugin_post_install_callback = isset( $plugins_list['wp_slugs'][ $plugin ]['post_install_callback'] )
88        ? $plugins_list['wp_slugs'][ $plugin ]['post_install_callback']
89        : false;
90        if ( ! self::is_plugin_installed( $plugin_path ) ) {
91            $status = self::install_from_wordpress( $plugin, $should_activate );
92            if ( \is_wp_error( $status ) ) {
93                return $status;
94            }
95            if ( is_callable( $plugin_post_install_callback ) ) {
96                $plugin_post_install_callback();
97            }
98        }
99
100        if ( $should_activate && ! \is_plugin_active( $plugin_path ) ) {
101            $status = \activate_plugin( $plugin_path );
102            if ( \is_wp_error( $status ) ) {
103                $status->add_data( array( 'status' => 500 ) );
104
105                return $status;
106            }
107        }
108
109        return new \WP_REST_Response(
110            array(),
111            201
112        );
113    }
114
115    /**
116     * Install a plugin from wordpress.org.
117     *
118     * @param string  $plugin The wp_slug to install.
119     * @param boolean $should_activate Whether to activate the plugin after install.
120     * @return \WP_REST_Response|\WP_Error
121     */
122    public static function install_from_wordpress( $plugin, $should_activate = true ) {
123        require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
124
125        $api = \plugins_api(
126            'plugin_information',
127            array(
128                'slug'   => $plugin,
129                'fields' => array(
130                    'sections'       => false,
131                    'language_packs' => true,
132                ),
133            )
134        );
135
136        if ( is_wp_error( $api ) ) {
137            if ( false !== strpos( $api->get_error_message(), 'Plugin not found.' ) ) {
138                $api->add_data( array( 'status' => 404 ) );
139            } else {
140                $api->add_data( array( 'status' => 500 ) );
141            }
142
143            return $api;
144        }
145
146        $status = self::install_from_zip( $api->download_link, $should_activate, $api->language_packs );
147        if ( \is_wp_error( $status ) ) {
148            return $status;
149        }
150
151        return new \WP_REST_Response(
152            array(),
153            200
154        );
155    }
156
157    /**
158     * Provisions a license and installs or activates a premium plugin.
159     *
160     * @param string  $plugin The slug of the premium plugin.
161     * @param string  $provider The provider name for the premium plugin.
162     * @param boolean $should_activate Whether to activate the plugin after installation. (default: true)
163     * @param mixed   $plugin_basename The plugin basename, if known. (default: false)
164     *
165     * @return \WP_Error|\WP_REST_Response
166     */
167    public static function install_premium_plugin( $plugin, $provider, $should_activate = true, $plugin_basename = false ) {
168        $is_installed = false;
169        $is_active    = false;
170
171        // Ensure plugin and provider are not empty
172        if ( empty( $plugin ) || empty( $provider ) ) {
173            return new \WP_Error(
174                'nfd_installer_error',
175                __( 'Plugin slug and provider name cannot be empty.', 'wp-module-installer' )
176            );
177        }
178
179        $pls_utility = new PLSUtility();
180
181        // Provision a license for the premium plugin, this returns basename and download URL
182        $license_response = $pls_utility->provision_license( $plugin, $provider );
183        if ( is_wp_error( $license_response ) ) {
184            $license_response->add(
185                'nfd_installer_error',
186                __( 'Failed to provision license for premium plugin: ', 'wp-module-installer' ) . $plugin,
187                array(
188                    'plugin'   => $plugin,
189                    'provider' => $provider,
190                )
191            );
192            return $license_response;
193        }
194
195        // Maybe get the plugin basename from the license response
196        // This is only returned if the plugin is already installed and licensed
197        $plugin_basename = ! empty( $license_response['basename'] ) ? $license_response['basename'] : false;
198
199        // Check if the plugin is already installed
200        if ( $plugin_basename && self::is_plugin_installed( $plugin_basename ) ) {
201            $is_installed = true;
202        }
203        // If NOT installed, install plugin
204        if ( ! $is_installed ) {
205            // Check if the download URL is present in the license response
206            if ( empty( $license_response['downloadUrl'] ) ) {
207                return new \WP_Error(
208                    'nfd_installer_error',
209                    __( 'Download URL is missing for premium plugin: ', 'wp-module-installer' ) . $plugin,
210                    array(
211                        'plugin'   => $plugin,
212                        'provider' => $provider,
213                    )
214                );
215            }
216            $install_status = self::install_from_zip( $license_response['downloadUrl'], $should_activate );
217            if ( is_wp_error( $install_status ) ) {
218                $install_status->add(
219                    'nfd_installer_error',
220                    __( 'Failed to install or activate the premium plugin: ', 'wp-module-installer' ) . $plugin,
221                    array(
222                        'plugin'       => $plugin,
223                        'provider'     => $provider,
224                        'download_url' => $license_response['downloadUrl'],
225                    )
226                );
227                return $install_status;
228            }
229        }
230
231        // Check if the plugin is already active
232        // Can only be true if the plugin was already installed
233        // Only need to check if it should be activated
234        if ( $is_installed && $should_activate && is_plugin_active( $plugin_basename ) ) {
235            $is_active = true;
236        }
237        // If should activate, and not already active, activate the plugin
238        if ( $is_installed && $should_activate && ! $is_active ) {
239            $activate_plugin_response = activate_plugin( $plugin_basename );
240            if ( is_wp_error( $activate_plugin_response ) ) {
241                $activate_plugin_response->add(
242                    'nfd_installer_error',
243                    __( 'Failed to activate the plugin: ', 'wp-module-installer' ) . $plugin,
244                    array(
245                        'plugin'   => $plugin,
246                        'provider' => $provider,
247                        'basename' => $plugin_basename,
248                    )
249                );
250                return $activate_plugin_response;
251            }
252        }
253
254        // Activate the license
255        // Should we do this here or let the activation hook handle it - see WPAdmin/Listeners/InstallerListener.php
256        $license_activation_response = $pls_utility->activate_license( $plugin );
257        if ( is_wp_error( $license_activation_response ) ) {
258            $license_activation_response->add(
259                'nfd_installer_error',
260                __( 'Failed to activate the license for the premium plugin: ', 'wp-module-installer' ) . $plugin,
261                array(
262                    'plugin'   => $plugin,
263                    'provider' => $provider,
264                )
265            );
266            return $license_activation_response;
267        }
268
269        // Return success response
270        return new \WP_REST_Response(
271            array(
272                'message' => __( 'Successfully provisioned and installed: ', 'wp-module-installer' ) . $plugin,
273            ),
274            200
275        );
276    }
277
278    /**
279     * Install the plugin from a custom ZIP.
280     *
281     * @param string  $url The ZIP URL to install from.
282     * @param boolean $should_activate Whether to activate the plugin after install.
283     * @param array   $language_packs The set of language packs to install for the plugin.
284     * @return \WP_REST_Response|\WP_Error
285     */
286    public static function install_from_zip( $url, $should_activate, $language_packs = array() ) {
287        require_once ABSPATH . 'wp-admin/includes/file.php';
288        require_once ABSPATH . 'wp-admin/includes/misc.php';
289        require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
290
291        \wp_cache_flush();
292        $skin     = new \WP_Ajax_Upgrader_Skin();
293        $upgrader = new \Plugin_Upgrader( $skin );
294
295        $result = $upgrader->install( $url );
296        if ( \is_wp_error( $result ) ) {
297            $result->add_data( array( 'status' => 500 ) );
298
299            return $result;
300        }
301        if ( \is_wp_error( $skin->result ) ) {
302            $skin->result->add_data( array( 'status' => 500 ) );
303
304            return $skin->result;
305        }
306        if ( $skin->get_errors()->has_errors() ) {
307            $error = $skin->get_errors();
308            $error->add_data( array( 'status' => 500 ) );
309
310            return $error;
311        }
312        if ( is_null( $result ) ) {
313            // Pass through the error from WP_Filesystem if one was raised.
314            if ( $wp_filesystem instanceof \WP_Filesystem_Base
315                && \is_wp_error( $wp_filesystem->errors ) && $wp_filesystem->errors->has_errors()
316            ) {
317                return new \WP_Error(
318                    'unable_to_connect_to_filesystem',
319                    $wp_filesystem->errors->get_error_message(),
320                    array( 'status' => 500 )
321                );
322            }
323
324            return new \WP_Error(
325                'unable_to_connect_to_filesystem',
326                __( 'Unable to connect to the filesystem.', 'wp-module-installer' ),
327                array( 'status' => 500 )
328            );
329        }
330
331        $plugin_file = $upgrader->plugin_info();
332        if ( ! $plugin_file ) {
333            return new \WP_Error(
334                'unable_to_determine_installed_plugin',
335                __( 'Unable to determine what plugin was installed.', 'wp-module-installer' ),
336                array( 'status' => 500 )
337            );
338        }
339
340        if ( $should_activate && ! \is_plugin_active( $plugin_file ) ) {
341            $status = \activate_plugin( $plugin_file );
342            if ( \is_wp_error( $status ) ) {
343                $status->add_data( array( 'status' => 500 ) );
344
345                return $status;
346            }
347        }
348
349        // Install translations.
350        $installed_locales = array_values( get_available_languages() );
351        /** This filter is documented in wp-includes/update.php */
352        $installed_locales = apply_filters( 'plugins_update_check_locales', $installed_locales );
353
354        if ( ! empty( $language_packs ) ) {
355            $language_packs = array_map(
356                static function ( $item ) {
357                    return (object) $item;
358                },
359                $language_packs
360            );
361
362            $language_packs = array_filter(
363                $language_packs,
364                static function ( $pack ) use ( $installed_locales ) {
365                    return in_array( $pack->language, $installed_locales, true );
366                }
367            );
368
369            if ( $language_packs ) {
370                $lp_upgrader = new \Language_Pack_Upgrader( $skin );
371
372                // Install all applicable language packs for the plugin.
373                $lp_upgrader->bulk_upgrade( $language_packs );
374            }
375        }
376
377        return new \WP_REST_Response(
378            array(),
379            200
380        );
381    }
382
383    /**
384     * Checks if a given slug is a valid nfd_slug. Ref: includes/Data/Plugins.php for nfd_slug.
385     *
386     * @param string $plugin Slug of the plugin.
387     * @return boolean
388     */
389    public static function is_nfd_slug( $plugin ) {
390        $plugins_list = Plugins::get();
391        if ( isset( $plugins_list['nfd_slugs'][ $plugin ]['approved'] ) ) {
392            return true;
393        }
394        return false;
395    }
396
397    /**
398     * Determines if a plugin has already been installed.
399     *
400     * @param string $plugin_path Path to the plugin's header file.
401     * @return boolean
402     */
403    public static function is_plugin_installed( $plugin_path ) {
404        if ( ! function_exists( 'get_plugins' ) ) {
405            require_once ABSPATH . 'wp-admin/includes/plugin.php';
406        }
407        $all_plugins = \get_plugins();
408        if ( ! empty( $all_plugins[ $plugin_path ] ) ) {
409            return true;
410        } else {
411            return false;
412        }
413    }
414
415    /**
416     * Get the type of plugin slug. Ref: includes/Data/Plugins.php for the different types.
417     *
418     * @param string $plugin The plugin slug to retrieve the type.
419     * @return string
420     */
421    public static function get_plugin_type( $plugin ) {
422        if ( \wp_http_validate_url( $plugin ) ) {
423            return 'urls';
424        }
425        if ( self::is_nfd_slug( $plugin ) ) {
426            return 'nfd_slugs';
427        }
428        return 'wp_slugs';
429    }
430
431    /**
432     * Get the path to the Plugin's header file.
433     *
434     * @param string $plugin The slug of the plugin.
435     * @param string $plugin_type The type of plugin.
436     * @return string|false
437     */
438    public static function get_plugin_path( $plugin, $plugin_type ) {
439        $plugin_list = Plugins::get();
440        return isset( $plugin_list[ $plugin_type ][ $plugin ] ) ? $plugin_list[ $plugin_type ][ $plugin ]['path'] : false;
441    }
442
443    /**
444     * Checks if a plugin with the given slug and activation criteria already exists.
445     *
446     * @param string  $plugin The slug of the plugin to check for
447     * @param boolean $is_active The activation criteria.
448     * @return boolean
449     */
450    public static function exists( $plugin, $is_active ) {
451        $plugin_type = self::get_plugin_type( $plugin );
452        $plugin_path = self::get_plugin_path( $plugin, $plugin_type );
453        if ( ! ( $plugin_path && self::is_plugin_installed( $plugin_path ) ) ) {
454            return false;
455        }
456
457        if ( $is_active && ! \is_plugin_active( $plugin_path ) ) {
458            return false;
459        }
460        return true;
461    }
462
463    /**
464     * Checks if a give plugin is active, given the plugin path.
465     *
466     * @param string $plugin_path The plugin path.
467     * @return boolean
468     */
469    public static function is_active( $plugin_path ) {
470        return \is_plugin_active( $plugin_path );
471    }
472
473    /**
474     * Activates a plugin slug after performing the necessary checks.
475     *
476     * @param string $plugin The plugin slug. Ref: includes/Data/Plugins.php for the slugs.
477     * @return boolean
478     */
479    public static function activate( $plugin ) {
480        $plugin_type = self::get_plugin_type( $plugin );
481        $plugin_path = self::get_plugin_path( $plugin, $plugin_type );
482        if ( ! ( $plugin_path && self::is_plugin_installed( $plugin_path ) ) ) {
483            return false;
484        }
485
486        if ( \is_plugin_active( $plugin_path ) ) {
487            return true;
488        }
489
490        return \activate_plugin( $plugin_path );
491        // handle post install callback here.
492    }
493
494    /**
495     * Deactivates a given plugin slug after performing the necessary checks.
496     *
497     * @param string $plugin The plugin slug. Ref: includes/Data/Plugins.php for the slugs.
498     * @return boolean
499     */
500    public static function deactivate( $plugin ) {
501        $plugin_type = self::get_plugin_type( $plugin );
502        $plugin_path = self::get_plugin_path( $plugin, $plugin_type );
503        if ( ! ( $plugin_path && self::is_plugin_installed( $plugin_path ) ) ) {
504            return false;
505        }
506
507        if ( ! \is_plugin_active( $plugin_path ) ) {
508            return true;
509        }
510
511        \deactivate_plugins( $plugin_path );
512        return true;
513        // handle post install callback here.
514    }
515
516    /**
517     * Install the Endurance Page Cache Plugin
518     *
519     * [TODO] Make this generic for mu-plugins and direct file downloads.
520     *
521     * @return \WP_REST_Response|\WP_Error
522     */
523    public static function install_endurance_page_cache() {
524        if ( ! self::connect_to_filesystem() ) {
525            return new \WP_Error(
526                'nfd_installer_error',
527                __( 'Could not connect to the filesystem.', 'wp-module-installer' ),
528                array( 'status' => 500 )
529            );
530        }
531
532        global $wp_filesystem;
533
534        $plugin_list = Plugins::get();
535        $plugin_url  = $plugin_list['nfd_slugs']['nfd_slug_endurance_page_cache']['url'];
536        $plugin_path = $plugin_list['nfd_slugs']['nfd_slug_endurance_page_cache']['path'];
537
538        if ( $wp_filesystem->exists( $plugin_path ) ) {
539            return new \WP_REST_Response(
540                array(),
541                200
542            );
543        }
544
545        if ( ! $wp_filesystem->is_dir( WP_CONTENT_DIR . '/mu-plugins' ) ) {
546            $wp_filesystem->mkdir( WP_CONTENT_DIR . '/mu-plugins' );
547        }
548
549        $request = \wp_remote_get( $plugin_url );
550        if ( \is_wp_error( $request ) ) {
551            return $request;
552        }
553
554        $wp_filesystem->put_contents( $plugin_path, $request['body'], FS_CHMOD_FILE );
555
556        return new \WP_REST_Response(
557            array(),
558            200
559        );
560    }
561
562    /**
563     * Establishes a connection to the wp_filesystem.
564     *
565     * @return boolean
566     */
567    protected static function connect_to_filesystem() {
568        require_once ABSPATH . 'wp-admin/includes/file.php';
569
570        // We want to ensure that the user has direct access to the filesystem.
571        $access_type = \get_filesystem_method();
572        if ( 'direct' !== $access_type ) {
573            return false;
574        }
575
576        $creds = \request_filesystem_credentials( site_url() . '/wp-admin', '', false, false, array() );
577
578        if ( ! \WP_Filesystem( $creds ) ) {
579            return false;
580        }
581
582        return true;
583    }
584
585        /**
586         * Verify caller has permissions to install plugins.
587         *
588         * @param \WP_REST_Request $request the incoming request object.
589         *
590         * @return boolean
591         */
592    public static function check_install_permissions( \WP_REST_Request $request ) {
593        $install_hash = $request->get_header( 'X-NFD-INSTALLER' );
594        return self::rest_verify_plugin_install_hash( $install_hash )
595            && Permissions::rest_is_authorized_admin();
596    }
597
598        /**
599         * Retrieve Plugin Install Hash Value.
600         *
601         * @return string
602         */
603    public static function rest_get_plugin_install_hash() {
604        return 'NFD_INSTALLER_' . hash( 'sha256', NFD_INSTALLER_VERSION . wp_salt( 'nonce' ) . site_url() );
605    }
606
607    /**
608     * Verify Plugin Install Hash Value.
609     *
610     * @param string $hash Hash Value.
611     * @return boolean
612     */
613    public static function rest_verify_plugin_install_hash( $hash ) {
614        return self::rest_get_plugin_install_hash() === $hash;
615    }
616
617    /**
618     * Retrieves the current status of a plugin.
619     *
620     * @param string $plugin The slug or identifier of the plugin.
621     *
622     * @return string
623     */
624    public static function get_plugin_status( $plugin ) {
625        $plugin_type         = self::get_plugin_type( $plugin );
626        $plugin_path         = self::get_plugin_path( $plugin, $plugin_type );
627        $plugin_status_codes = Plugins::get_status_codes();
628
629        if ( ! $plugin_path ) {
630            return $plugin_status_codes['unknown'];
631        }
632
633        if ( is_plugin_active( $plugin_path ) ) {
634            return $plugin_status_codes['active'];
635        }
636
637        if ( self::is_plugin_installed( $plugin_path ) ) {
638            return $plugin_status_codes['installed'];
639        }
640
641        return $plugin_status_codes['not_installed'];
642    }
643}