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