diff --git a/assets/js/src/settings/premium_tab/premium_tab.jsx b/assets/js/src/settings/premium_tab/premium_tab.jsx index 59198fc697..86ad84d93f 100644 --- a/assets/js/src/settings/premium_tab/premium_tab.jsx +++ b/assets/js/src/settings/premium_tab/premium_tab.jsx @@ -7,15 +7,6 @@ import MssMessages from 'settings/premium_tab/messages/mss_messages.jsx'; import { PremiumStatus, PremiumMessages } from 'settings/premium_tab/messages/premium_messages.jsx'; import { PremiumInstallationStatus } from 'settings/premium_tab/messages/premium_installation_messages.jsx'; -const request = async (url) => { - try { - const response = await fetch(url); - return response.ok; - } catch (error) { - return false; - } -}; - const requestServicesApi = async (key, action) => MailPoet.Ajax.post({ api_version: window.mailpoet_api_version, endpoint: 'services', @@ -23,6 +14,12 @@ const requestServicesApi = async (key, action) => MailPoet.Ajax.post({ data: { key }, }); +const requestPremiumApi = async (action) => MailPoet.Ajax.post({ + api_version: window.mailpoet_api_version, + endpoint: 'premium', + action, +}); + const activateMss = async (key) => MailPoet.Ajax.post({ api_version: window.mailpoet_api_version, endpoint: 'settings', @@ -47,8 +44,6 @@ const PremiumTab = (props) => { const [mssKeyValid, setMssKeyValid] = useState(key ? props.mssKeyValid : null); const [mssKeyMessage, setMssKeyMessage] = useState(null); - let premiumActivateUrl = props.premiumActivateUrl; - // key is considered valid if either Premium or MSS check passes const keyValid = useMemo(() => { if (premiumStatus > PremiumStatus.KEY_INVALID || mssKeyValid) { @@ -64,7 +59,9 @@ const PremiumTab = (props) => { const errorStatus = isAfterInstall ? status.INSTALL_ACTIVATING_ERROR : status.ACTIVATE_ERROR; setPremiumInstallationStatus(activateStatus); - if (!await request(premiumActivateUrl)) { + try { + await requestPremiumApi('activatePlugin'); + } catch (error) { setPremiumInstallationStatus(errorStatus); return; } @@ -73,15 +70,8 @@ const PremiumTab = (props) => { const installPremiumPlugin = async () => { setPremiumInstallationStatus(PremiumInstallationStatus.INSTALL_INSTALLING); - if (!await request(props.premiumInstallUrl)) { - setPremiumInstallationStatus(PremiumInstallationStatus.INSTALL_INSTALLING_ERROR); - return; - } - - // refetch 'plugin_activate_url' since it's only set after installation try { - const response = await requestServicesApi(key, 'checkPremiumKey'); - premiumActivateUrl = response.meta.premium_activate_url; + await requestPremiumApi('installPlugin'); } catch (error) { setPremiumInstallationStatus(PremiumInstallationStatus.INSTALL_INSTALLING_ERROR); return; @@ -228,13 +218,10 @@ PremiumTab.propTypes = { premiumStatus: PropTypes.number.isRequired, mssKeyValid: PropTypes.bool.isRequired, premiumPluginActive: PropTypes.bool.isRequired, - premiumInstallUrl: PropTypes.string.isRequired, - premiumActivateUrl: PropTypes.string, }; PremiumTab.defaultProps = { activationKey: null, - premiumActivateUrl: null, }; const container = document.getElementById('settings-premium-tab'); @@ -260,8 +247,6 @@ if (container) { premiumStatus={getPremiumStatus()} mssKeyValid={window.mailpoet_mss_key_valid} premiumPluginActive={!!window.mailpoet_premium_version} - premiumInstallUrl={window.mailpoet_premium_install_url} - premiumActivateUrl={window.mailpoet_premium_activate_url || null} />, container ); diff --git a/lib/API/JSON/v1/Premium.php b/lib/API/JSON/v1/Premium.php new file mode 100644 index 0000000000..d1c4375883 --- /dev/null +++ b/lib/API/JSON/v1/Premium.php @@ -0,0 +1,74 @@ + AccessControl::PERMISSION_MANAGE_SETTINGS, + ]; + + /** @var ServicesChecker */ + private $services_checker; + + /** @var WPFunctions */ + private $wp; + + public function __construct( + ServicesChecker $services_checker, + WPFunctions $wp + ) { + $this->services_checker = $services_checker; + $this->wp = $wp; + } + + public function installPlugin() { + $premium_key_valid = $this->services_checker->isPremiumKeyValid(false); + if (!$premium_key_valid) { + return $this->error($this->wp->__('Premium key is not valid.', 'mailpoet')); + } + + $plugin_info = $this->wp->pluginsApi('plugin_information', [ + 'slug' => self::PREMIUM_PLUGIN_SLUG, + ]); + + if (!$plugin_info || $plugin_info instanceof WP_Error) { + return $this->error($this->wp->__('Error when installing MailPoet Premium plugin.', 'mailpoet')); + } + + $plugin_info = (array)$plugin_info; + $result = $this->wp->installPlugin($plugin_info['download_link']); + if ($result !== true) { + return $this->error($this->wp->__('Error when installing MailPoet Premium plugin.', 'mailpoet')); + } + return $this->successResponse(); + } + + public function activatePlugin() { + $premium_key_valid = $this->services_checker->isPremiumKeyValid(false); + if (!$premium_key_valid) { + return $this->error($this->wp->__('Premium key is not valid.', 'mailpoet')); + } + + $result = $this->wp->activatePlugin(self::PREMIUM_PLUGIN_PATH); + if ($result !== null) { + return $this->error($this->wp->__('Error when activating MailPoet Premium plugin.', 'mailpoet')); + } + return $this->successResponse(); + } + + private function error($message) { + return $this->badRequest([ + APIError::BAD_REQUEST => $message, + ]); + } +} diff --git a/lib/DI/ContainerConfigurator.php b/lib/DI/ContainerConfigurator.php index 72a0a42ae0..6dba1015f1 100644 --- a/lib/DI/ContainerConfigurator.php +++ b/lib/DI/ContainerConfigurator.php @@ -76,6 +76,7 @@ class ContainerConfigurator implements IContainerConfigurator { $container->autowire(\MailPoet\API\JSON\v1\Newsletters::class)->setPublic(true); $container->autowire(\MailPoet\API\JSON\v1\NewsletterLinks::class)->setPublic(true); $container->autowire(\MailPoet\API\JSON\v1\NewsletterTemplates::class)->setPublic(true); + $container->autowire(\MailPoet\API\JSON\v1\Premium::class)->setPublic(true); $container->autowire(\MailPoet\API\JSON\v1\Segments::class)->setPublic(true); $container->autowire(\MailPoet\API\JSON\v1\SendingQueue::class)->setPublic(true); $container->autowire(\MailPoet\API\JSON\v1\Services::class)->setPublic(true); diff --git a/lib/WP/Functions.php b/lib/WP/Functions.php index d201de472f..9cae2d7a39 100644 --- a/lib/WP/Functions.php +++ b/lib/WP/Functions.php @@ -2,6 +2,8 @@ namespace MailPoet\WP; +use Plugin_Upgrader; +use WP_Ajax_Upgrader_Skin; use WP_Error; class Functions { @@ -562,6 +564,40 @@ class Functions { return wpautop($pee, $br); } + /** + * @param string $action + * @param array|object $args + * @return object|array|WP_Error + */ + public function pluginsApi($action, $args = []) { + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + return plugins_api($action, $args); + } + + /** + * @param string $package + * @param array $args + * @return bool|WP_Error + */ + public function installPlugin($package, $args = []) { + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php'; + $upgrader = new Plugin_Upgrader(new WP_Ajax_Upgrader_Skin()); + return $upgrader->install($package, $args); + } + + /** + * @param string $plugin + * @param string $redirect + * @param bool $network_wide + * @param bool $silent + * @return WP_Error|null + */ + public function activatePlugin($plugin, $redirect = '', $network_wide = false, $silent = false) { + return activate_plugin($plugin, $redirect, $network_wide, $silent); + } + /** * @param string $host * @return array|bool diff --git a/tasks/phpstan/bootstrap.php b/tasks/phpstan/bootstrap.php index 6b4f429db1..356b92ee3f 100644 --- a/tasks/phpstan/bootstrap.php +++ b/tasks/phpstan/bootstrap.php @@ -16,3 +16,38 @@ if (!class_exists(WooCommerce::class)) { } require_once __DIR__ . '/function-stubs.php'; + +// methods & classes for Premium plugin installation are required +// only when needed so we need to let PHPStan know about them +if (!function_exists('plugins_api')) { + /** + * @param string $action + * @param array|object $args + * @return object|array|WP_Error + */ + function plugins_api($action, $args) { + return []; + } +} + +if (!class_exists(WP_Ajax_Upgrader_Skin::class)) { + // phpcs:ignore + class WP_Ajax_Upgrader_Skin {} +} + +if (!class_exists(Plugin_Upgrader::class)) { + // phpcs:ignore + class Plugin_Upgrader { + public function __construct($skin = null) { + } + + /** + * @param string $package + * @param array $args + * @return bool|WP_Error + */ + public function install($package, $args = []) { + return true; + } + } +} diff --git a/tests/unit/API/JSON/PremiumTest.php b/tests/unit/API/JSON/PremiumTest.php new file mode 100644 index 0000000000..6727da57a3 --- /dev/null +++ b/tests/unit/API/JSON/PremiumTest.php @@ -0,0 +1,134 @@ +makeEmpty(ServicesChecker::class, [ + 'isPremiumKeyValid' => Expected::once(true), + ]); + + $wp = $this->make(WPFunctions::class, [ + 'pluginsApi' => Expected::once([ + 'download_link' => 'https://some-download-link', + ]), + 'installPlugin' => Expected::once(true), + ]); + + $premium = new Premium($services_checker, $wp); + $response = $premium->installPlugin(); + expect($response)->isInstanceOf(SuccessResponse::class); + } + + public function testInstallationFailsWhenKeyInvalid() { + $services_checker = $this->makeEmpty(ServicesChecker::class, [ + 'isPremiumKeyValid' => Expected::once(false), + ]); + + $wp = $this->make(WPFunctions::class, [ + 'pluginsApi' => Expected::never(), + 'installPlugin' => Expected::never(), + ]); + + $premium = new Premium($services_checker, $wp); + $response = $premium->installPlugin(); + expect($response)->isInstanceOf(ErrorResponse::class); + expect($response->getData()['errors'][0])->same([ + 'error' => 'bad_request', + 'message' => 'Premium key is not valid.', + ]); + } + + public function testInstallationFailsWhenNoPluginInfo() { + $services_checker = $this->makeEmpty(ServicesChecker::class, [ + 'isPremiumKeyValid' => Expected::once(true), + ]); + + $wp = $this->make(WPFunctions::class, [ + 'pluginsApi' => Expected::once(null), + 'installPlugin' => Expected::never(), + ]); + + $premium = new Premium($services_checker, $wp); + $response = $premium->installPlugin(); + expect($response)->isInstanceOf(ErrorResponse::class); + expect($response->getData()['errors'][0])->same([ + 'error' => 'bad_request', + 'message' => 'Error when installing MailPoet Premium plugin.', + ]); + } + + public function testInstallationFailsOnError() { + $services_checker = $this->makeEmpty(ServicesChecker::class, [ + 'isPremiumKeyValid' => Expected::once(true), + ]); + + $wp = $this->make(WPFunctions::class, [ + 'pluginsApi' => Expected::once([ + 'download_link' => 'https://some-download-link', + ]), + 'installPlugin' => Expected::once(false), + ]); + + $premium = new Premium($services_checker, $wp); + $response = $premium->installPlugin(); + expect($response)->isInstanceOf(ErrorResponse::class); + expect($response->getData()['errors'][0])->same([ + 'error' => 'bad_request', + 'message' => 'Error when installing MailPoet Premium plugin.', + ]); + } + + public function testItActivatesPlugin() { + $services_checker = $this->makeEmpty(ServicesChecker::class, [ + 'isPremiumKeyValid' => Expected::once(true), + ]); + + $wp = $this->make(WPFunctions::class, [ + 'activatePlugin' => Expected::once(null), + ]); + + $premium = new Premium($services_checker, $wp); + $response = $premium->activatePlugin(); + expect($response)->isInstanceOf(SuccessResponse::class); + } + + public function testActivationFailsWhenKeyInvalid() { + $services_checker = $this->makeEmpty(ServicesChecker::class, [ + 'isPremiumKeyValid' => Expected::once(false), + ]); + + $premium = new Premium($services_checker, new WPFunctions()); + $response = $premium->activatePlugin(); + expect($response)->isInstanceOf(ErrorResponse::class); + expect($response->getData()['errors'][0])->same([ + 'error' => 'bad_request', + 'message' => 'Premium key is not valid.', + ]); + } + + public function testActivationFailsOnError() { + $services_checker = $this->makeEmpty(ServicesChecker::class, [ + 'isPremiumKeyValid' => Expected::once(true), + ]); + + $wp = $this->make(WPFunctions::class, [ + 'activatePlugin' => Expected::once('error'), + ]); + + $premium = new Premium($services_checker, $wp); + $response = $premium->activatePlugin(); + expect($response)->isInstanceOf(ErrorResponse::class); + expect($response->getData()['errors'][0])->same([ + 'error' => 'bad_request', + 'message' => 'Error when activating MailPoet Premium plugin.', + ]); + } +} diff --git a/views/settings/premium.html b/views/settings/premium.html index 81f27d7f4a..3a617788c8 100644 --- a/views/settings/premium.html +++ b/views/settings/premium.html @@ -4,8 +4,6 @@ var mailpoet_premium_key_valid = <%= json_encode(premium_key_valid) %>; var mailpoet_premium_plugin_installed = <%= json_encode(premium_plugin_installed) %>; var mailpoet_mss_key_valid = <%= json_encode(mss_key_valid) %>; - var mailpoet_premium_install_url = <%= json_encode(premium_install_url) %>; - var mailpoet_premium_activate_url = <%= json_encode(premium_activate_url) %>; <% endautoescape %>