From 943e9f18697f137cf0025d06fad87f2e420af3c0 Mon Sep 17 00:00:00 2001 From: Denis Dvali Date: Mon, 17 Apr 2023 11:51:17 +0400 Subject: [PATCH] Initial version of plugin installer version 2. --- .../class-wc-wccom-site-installer.php | 36 ++- .../wccom-site/class-wc-wccom-site.php | 63 +++--- ...ass-wc-wccom-site-installation-manager.php | 139 ++++++++++++ ...-wccom-site-installation-state-storage.php | 65 ++++++ ...class-wc-wccom-site-installation-state.php | 141 ++++++++++++ ...ass-installation-step-activate-product.php | 101 +++++++++ ...ass-installation-step-download-product.php | 26 +++ .../class-installation-step-move-product.php | 44 ++++ .../class-installation-step-product-info.php | 81 +++++++ ...class-installation-step-unpack-product.php | 26 +++ .../interface-installaton-step.php | 9 + ...-rest-wccom-site-installer-error-codes.php | 90 ++++++++ ...ass-wc-rest-wccom-site-installer-error.php | 36 +++ ...ss-wc-rest-wccom-site-installer-errors.php | 73 ------ ...est-wccom-site-installer-controller-v2.php | 207 ++++++++++++++++++ ...c-rest-wccom-site-installer-controller.php | 47 ++-- 16 files changed, 1045 insertions(+), 139 deletions(-) create mode 100644 plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-manager.php create mode 100644 plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-state-storage.php create mode 100644 plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-state.php create mode 100644 plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-activate-product.php create mode 100644 plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-download-product.php create mode 100644 plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-move-product.php create mode 100644 plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-product-info.php create mode 100644 plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-unpack-product.php create mode 100644 plugins/woocommerce/includes/wccom-site/installation/installation-steps/interface-installaton-step.php create mode 100644 plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-error-codes.php create mode 100644 plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-error.php delete mode 100644 plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php create mode 100644 plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller-v2.php diff --git a/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site-installer.php b/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site-installer.php index e066141f129..e4234383913 100644 --- a/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site-installer.php +++ b/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site-installer.php @@ -63,6 +63,8 @@ class WC_WCCOM_Site_Installer { 'activate_product', ); + private static $wp_upgrader; + /** * Get the product install state. * @@ -150,15 +152,7 @@ class WC_WCCOM_Site_Installer { * element is install args. */ public static function install( $products ) { - require_once ABSPATH . 'wp-admin/includes/file.php'; - require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; - require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; - require_once ABSPATH . 'wp-admin/includes/plugin.php'; - - WP_Filesystem(); - $upgrader = new WP_Upgrader( new Automatic_Upgrader_Skin() ); - $upgrader->init(); - wp_clean_plugins_cache(); + $upgrader = self::get_wp_upgrader(); foreach ( $products as $product_id => $install_args ) { self::install_product( $product_id, $install_args, $upgrader ); @@ -424,7 +418,7 @@ class WC_WCCOM_Site_Installer { * @param int $product_id Product ID. * @return \WP_Error|null */ - private static function activate_plugin( $product_id ) { + public static function activate_plugin( $product_id ) { // Clear plugins cache used in `WC_Helper::get_local_woo_plugins`. wp_clean_plugins_cache(); $filename = false; @@ -520,7 +514,7 @@ class WC_WCCOM_Site_Installer { * @param string $dir Directory name of the plugin. * @return bool|string */ - private static function get_wporg_plugin_main_file( $dir ) { + public static function get_wporg_plugin_main_file( $dir ) { // Ensure that exact dir name is used. $dir = trailingslashit( $dir ); @@ -546,7 +540,7 @@ class WC_WCCOM_Site_Installer { * @param string $dir Directory name of the plugin. * @return bool|array */ - private static function get_plugin_info( $dir ) { + public static function get_plugin_info( $dir ) { $plugin_folder = basename( $dir ); if ( ! function_exists( 'get_plugins' ) ) { @@ -574,4 +568,22 @@ class WC_WCCOM_Site_Installer { } return false; } + + public static function get_wp_upgrader() { + if (!empty(self::$wp_upgrader)) { + return self::$wp_upgrader; + } + + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + + WP_Filesystem(); + self::$wp_upgrader = new WP_Upgrader( new Automatic_Upgrader_Skin() ); + self::$wp_upgrader->init(); + wp_clean_plugins_cache(); + + return self::$wp_upgrader; + } } diff --git a/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site.php b/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site.php index d09cb06982b..981b2f799a5 100644 --- a/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site.php +++ b/plugins/woocommerce/includes/wccom-site/class-wc-wccom-site.php @@ -6,6 +6,9 @@ * @since 3.7.0 */ +use WC_REST_WCCOM_Site_Installer_Error_Codes as Installer_Error_Codes; +use WC_REST_WCCOM_Site_Installer_Error as Installer_Error; + defined( 'ABSPATH' ) || exit; /** @@ -63,11 +66,7 @@ class WC_WCCOM_Site { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { - return new WP_Error( - WC_REST_WCCOM_Site_Installer_Errors::NO_ACCESS_TOKEN_CODE, - WC_REST_WCCOM_Site_Installer_Errors::NO_ACCESS_TOKEN_MESSAGE, - array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_ACCESS_TOKEN_HTTP_CODE ) - ); + return new Installer_Error( Installer_Error_Codes::NO_ACCESS_TOKEN ); } ); return false; @@ -81,11 +80,7 @@ class WC_WCCOM_Site { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { - return new WP_Error( - WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_CODE, - WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_MESSAGE, - array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_SIGNATURE_HTTP_CODE ) - ); + return new Installer_Error( Installer_Error_Codes::NO_SIGNATURE ); } ); return false; @@ -98,11 +93,7 @@ class WC_WCCOM_Site { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { - return new WP_Error( - WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_CODE, - WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_MESSAGE, - array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::SITE_NOT_CONNECTED_HTTP_CODE ) - ); + return new Installer_Error( Installer_Error_Codes::SITE_NOT_CONNECTED ); } ); return false; @@ -112,11 +103,7 @@ class WC_WCCOM_Site { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { - return new WP_Error( - WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_CODE, - WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_MESSAGE, - array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::INVALID_TOKEN_HTTP_CODE ) - ); + return new Installer_Error( Installer_Error_Codes::INVALID_TOKEN ); } ); return false; @@ -128,11 +115,7 @@ class WC_WCCOM_Site { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { - return new WP_Error( - WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_CODE, - WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_MESSAGE, - array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::REQUEST_VERIFICATION_FAILED_HTTP_CODE ) - ); + return new Installer_Error( Installer_Error_Codes::REQUEST_VERIFICATION_FAILED ); } ); return false; @@ -143,11 +126,7 @@ class WC_WCCOM_Site { add_filter( self::AUTH_ERROR_FILTER_NAME, function() { - return new WP_Error( - WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_CODE, - WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_MESSAGE, - array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::USER_NOT_FOUND_HTTP_CODE ) - ); + return new Installer_Error( Installer_Error_Codes::USER_NOT_FOUND ); } ); return false; @@ -239,13 +218,31 @@ class WC_WCCOM_Site { * @return array Registered namespaces. */ public static function register_rest_namespace( $namespaces ) { - require_once WC_ABSPATH . 'includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php'; - require_once WC_ABSPATH . 'includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php'; - $namespaces['wccom-site/v1'] = array( + require_once WC_ABSPATH . 'includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-error-codes.php'; + require_once WC_ABSPATH . 'includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-error.php'; + require_once WC_ABSPATH . 'includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php'; + require_once WC_ABSPATH . 'includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller-v2.php'; + + require_once WC_ABSPATH . 'includes/wccom-site/installation/class-wc-wccom-site-installation-state.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/class-wc-wccom-site-installation-state-storage.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/class-wc-wccom-site-installation-manager.php'; + + require_once WC_ABSPATH . 'includes/wccom-site/installation/installation-steps/interface-installaton-step.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/installation-steps/class-installation-step-product-info.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/installation-steps/class-installation-step-download-product.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/installation-steps/class-installation-step-unpack-product.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/installation-steps/class-installation-step-move-product.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/installation-steps/class-installation-step-activate-product.php'; + + $namespaces['wccom-site/v1'] = array( 'installer' => 'WC_REST_WCCOM_Site_Installer_Controller', ); + $namespaces['wccom-site/v2'] = array( + 'installer' => 'WC_REST_WCCOM_Site_Installer_Controller_V2', + ); + return $namespaces; } } diff --git a/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-manager.php b/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-manager.php new file mode 100644 index 00000000000..566b2b27625 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-manager.php @@ -0,0 +1,139 @@ +product_id = $product_id; + $this->idempotency_key = $idempotency_key; + } + + public function run_installation(string $run_until_step) : bool { + $state = WC_WCCOM_Site_Installation_State_Storage::get_state( $this->product_id ); + + if ( $state && $state->get_idempotency_key() !== $this->idempotency_key ) { + throw new Installer_Error(Installer_Error_Codes::IDEMPOTENCY_KEY_MISMATCH); + } + + if ( ! $state ) { + $state = WC_WCCOM_Site_Installation_State::initiate_new( $this->product_id, $this->idempotency_key ); + } + + $this->can_run_installation( $run_until_step, $state ); + + $next_step = $this->get_next_step($state); + $installation_steps = $this->get_installation_steps( $next_step, $run_until_step ); + + array_walk($installation_steps, function($step_name) use ($state) { + $this->run_step($step_name, $state); + }); + + return true; + } + + public function reset_installation() : bool { + $state = WC_WCCOM_Site_Installation_State_Storage::get_state( $this->product_id ); + + if ( ! $state ) { + throw new Installer_Error( Installer_Error_Codes::NO_INITIATED_INSTALLATION_FOUND ); + } + + if ( $state->get_idempotency_key() !== $this->idempotency_key ) { + throw new Installer_Error(Installer_Error_Codes::IDEMPOTENCY_KEY_MISMATCH); + } + + $result = WC_WCCOM_Site_Installation_State_Storage::delete_state( $state ); + if ( ! $result) { + throw new Installer_Error(Installer_Error_Codes::FAILED_TO_RESET_INSTALLATION_STATE); + } + + return true; + } + + protected function can_run_installation( $run_until_step, $state ) { + // if ( $this->already_installed( $product_id )) { + // throw new Installer_Error(Installer_Error_Codes::PLUGIN_ALREADY_INSTALLED_MESSAGE ); + // } + + if ( $state->get_last_step_status() === \WC_WCCOM_Site_Installation_State::STEP_STATUS_IN_PROGRESS) { + throw new Installer_Error(Installer_Error_Codes::INSTALLATION_ALREADY_RUNNING ); + } + + if ( $state->get_last_step_status() === \WC_WCCOM_Site_Installation_State::STEP_STATUS_FAILED) { + throw new Installer_Error(Installer_Error_Codes::INSTALLATION_FAILED ); + } + + if ( $state->get_last_step_name() === self::STEPS[ count( self::STEPS ) - 1 ] ) { + throw new Installer_Error(Installer_Error_Codes::ALL_INSTALLATION_STEPS_RUN ); + } + + if (array_search($state->get_last_step_name(), self::STEPS) >= array_search($run_until_step, self::STEPS)) { + throw new Installer_Error(Installer_Error_Codes::REQUESTED_STEP_ALREADY_RUN ); + } + + if ( ! is_writable( WP_CONTENT_DIR ) ) { + throw new Installer_Error(Installer_Error_Codes::FILESYSTEM_REQUIREMENTS_NOT_MET ); + } + } + + protected function get_next_step($state) : string { + $last_executed_step = $state->get_last_step_name(); + + if (! $last_executed_step) { + return self::STEPS[0]; + } + + $last_executed_step_index = array_search($last_executed_step, self::STEPS); + + return self::STEPS[ $last_executed_step_index + 1 ]; + } + + protected function get_installation_steps(string $start_step, string $end_step) { + $start_step_offset = array_search($start_step, self::STEPS); + $end_step_index = array_search($end_step, self::STEPS); + $length = $end_step_index - $start_step_offset + 1; + + return array_slice(self::STEPS, $start_step_offset, $length); + } + + + protected function run_step($step_name, $state ) { + $state->initiate_step( $step_name ); + WC_WCCOM_Site_Installation_State_Storage::save_state( $state ); + + try { + $class_name = "WC_WCCOM_Site_Installation_Step_$step_name"; + $current_step = new $class_name( $state ); + $current_step->run(); + + } catch(Installer_Error $exception) { + + $state->capture_failure( $step_name, $exception->get_error_code() ); + WC_WCCOM_Site_Installation_State_Storage::save_state( $state ); + + throw $exception; + + } catch (Throwable $error) { + + $state->capture_failure( $step_name, Installer_Error_Codes::UNEXPECTED_ERROR ); + WC_WCCOM_Site_Installation_State_Storage::save_state( $state ); + + throw new Installer_Error(Installer_Error_Codes::UNEXPECTED_ERROR, $error->getMessage() ); + } + + $state->complete_step( $step_name ); + WC_WCCOM_Site_Installation_State_Storage::save_state( $state ); + } +} diff --git a/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-state-storage.php b/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-state-storage.php new file mode 100644 index 00000000000..544ed655aa4 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-state-storage.php @@ -0,0 +1,65 @@ +set_product_type( $data['product_type'] ?? null ); + $installation_state->set_product_name( $data['product_name'] ?? null); + $installation_state->set_download_url( $data['download_url'] ?? null); + $installation_state->set_download_path( $data['download_path'] ?? null); + $installation_state->set_unpacked_path( $data['unpacked_path'] ?? null); + $installation_state->set_installed_path( $data['installed_path'] ?? null ); + $installation_state->set_already_installed_plugin_info( $data['already_installed_plugin_info'] ?? null ); + + return $installation_state; + } + + public static function save_state( WC_WCCOM_Site_Installation_State $state) : bool { + $storage_key = self::get_storage_key( $state->get_product_id() ); + + return update_option($storage_key, [ + 'product_id' => $state->get_product_id(), + 'idempotency_key' => $state->get_idempotency_key(), + 'last_step_name' => $state->get_last_step_name(), + 'last_step_status' => $state->get_last_step_status(), + 'last_step_error' => $state->get_last_step_error(), + 'product_type' => $state->get_product_type(), + 'product_name' => $state->get_product_name(), + 'download_url' => $state->get_download_url(), + 'download_path' => $state->get_download_path(), + 'unpacked_path' => $state->get_unpacked_path(), + 'installed_path' => $state->get_installed_path(), + 'already_installed_plugin_info' => $state->get_already_installed_plugin_info(), + 'started_date' => $state->get_started_date(), + ]); + } + + public static function delete_state( WC_WCCOM_Site_Installation_State $state ) : bool { + $storage_key = self::get_storage_key( $state->get_product_id() ); + + return delete_option($storage_key); + } + + protected static function get_storage_key( $product_id ) : string { + return sprintf('wccom-product-installation-state-%d', $product_id); + } +} + diff --git a/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-state.php b/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-state.php new file mode 100644 index 00000000000..7ba39896f56 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-state.php @@ -0,0 +1,141 @@ +product_id = $product_id; + } + + public static function initiate_existing( $product_id, $idempotency_key, $last_step_name, $last_step_status, $last_step_error, $started_date ) { + $instance = new self( $product_id ); + $instance->idempotency_key = $idempotency_key; + $instance->last_step_name = $last_step_name; + $instance->last_step_status = $last_step_status; + $instance->last_step_error = $last_step_error; + $instance->started_date = $started_date; + + return $instance; + } + + public static function initiate_new( $product_id, $idempotency_key ) { + $instance = new self( $product_id ); + $instance->idempotency_key = $idempotency_key; + $instance->started_date = time(); + + return $instance; + } + + public function get_product_id() { + return $this->product_id; + } + + public function get_idempotency_key() { + return $this->idempotency_key; + } + + public function get_last_step_name() { + return $this->last_step_name; + } + + public function get_last_step_status( ) { + return $this->last_step_status; + } + + public function get_last_step_error( ) { + return $this->last_step_error; + } + + public function initiate_step( $step_name ) { + $this->last_step_name = $step_name; + $this->last_step_status = self::STEP_STATUS_IN_PROGRESS; + } + + public function complete_step( $step_name ) { + $this->last_step_name = $step_name; + $this->last_step_status = self::STEP_STATUS_COMPLETED; + } + public function capture_failure( $step_name, $error_code ) { + $this->last_step_name = $step_name; + $this->last_step_error = $error_code; + $this->last_step_status = self::STEP_STATUS_FAILED; + } + + public function get_product_type() { + return $this->product_type; + } + public function set_product_type( $product_type ) { + $this->product_type = $product_type; + } + + public function get_product_name() { + return $this->product_name; + } + + public function set_product_name( $product_name ) { + $this->product_name = $product_name; + } + + public function get_download_url() { + return $this->download_url; + } + + public function set_download_url( $download_url ) { + $this->download_url = $download_url; + } + + public function get_download_path( ) { + return $this->download_path; + } + + public function set_download_path( $download_path ) { + $this->download_path = $download_path; + } + + public function get_unpacked_path() { + return $this->unpacked_path; + } + + public function set_unpacked_path( $unpacked_path ) { + $this->unpacked_path = $unpacked_path; + } + + public function get_installed_path( ) { + return $this->installed_path; + } + + public function set_installed_path( $installed_path ) { + $this->installed_path = $installed_path; + } + + public function get_already_installed_plugin_info() { + return $this->already_installed_plugin_info; + } + + public function set_already_installed_plugin_info( $plugin_info ) { + $this->already_installed_plugin_info = $plugin_info; + } + + public function get_started_date() { + return $this->started_date; + } +} \ No newline at end of file diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-activate-product.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-activate-product.php new file mode 100644 index 00000000000..16c952ac25b --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-activate-product.php @@ -0,0 +1,101 @@ +state = $state; + } + + public function run( ) { + $product_id = $this->state->get_product_id(); + + if ( 'plugin' === $this->state->get_product_type() ) { + $this->activate_plugin( $product_id ); + } else { + $this->activate_theme( $product_id ); + } + + return $this->state; + } + + private function activate_plugin( $product_id ) { + // Clear plugins cache used in `WC_Helper::get_local_woo_plugins`. + wp_clean_plugins_cache(); + $filename = false; + + // If product is WP.org one, find out its filename. + $dir_name = $this->get_wporg_product_dir_name( ); + if ( false !== $dir_name ) { + $filename = \WC_WCCOM_Site_Installer::get_wporg_plugin_main_file( $dir_name ); + } + + if ( false === $filename ) { + $plugins = wp_list_filter( + WC_Helper::get_local_woo_plugins(), + [ + '_product_id' => $product_id, + ] + ); + + $filename = is_array( $plugins ) && ! empty( $plugins ) ? key( $plugins ) : ''; + } + + if ( empty( $filename ) ) { + return new Installer_Error( Installer_Error_Codes::UNKNOWN_FILENAME ); + } + + $result = activate_plugin( $filename ); + + if (is_wp_error($result)) { + return new Installer_Error( Installer_Error_Codes::PLUGIN_ACTIVATION_ERROR, $result->get_error_message() ); + } + } + + private function activate_theme( $product_id ) { + // Clear plugins cache used in `WC_Helper::get_local_woo_themes`. + wp_clean_themes_cache(); + $theme_slug = false; + + // If product is WP.org theme, find out its slug. + $dir_name = $this->get_wporg_product_dir_name( ); + if ( false !== $dir_name ) { + $theme_slug = basename( $dir_name ); + } + + if ( false === $theme_slug ) { + $themes = wp_list_filter( + WC_Helper::get_local_woo_themes(), + [ + '_product_id' => $product_id, + ] + ); + + $theme_slug = is_array( $themes ) && ! empty( $themes ) ? dirname( key( $themes ) ) : ''; + } + + if ( empty( $theme_slug ) ) { + return new Installer_Error( Installer_Error_Codes::UNKNOWN_FILENAME ); + } + + switch_theme( $theme_slug ); + } + + private function get_wporg_product_dir_name( ) { + if ( empty($this->state->get_installed_path() ) ) { + return false; + } + + // Check whether product was downloaded from WordPress.org. + $download_url = $this->state->get_download_url(); + $parsed_url = wp_parse_url( $download_url ); + if ( ! empty( $parsed_url['host'] ) && 'downloads.wordpress.org' !== $parsed_url['host'] ) { + return false; + } + + return basename( $this->state->get_installed_path() ); + } +} \ No newline at end of file diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-download-product.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-download-product.php new file mode 100644 index 00000000000..698de0936e7 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-download-product.php @@ -0,0 +1,26 @@ +state = $state; + } + + public function run( ) { + $upgrader = WC_WCCOM_Site_Installer::get_wp_upgrader(); + + $download_path = $upgrader->download_package( $this->state->get_download_url() ); + + if (empty($download_path)) { + throw new Installer_Error( Installer_Error_Codes::MISSING_DOWNLOAD_PATH); + } + + $this->state->set_download_path($download_path); + + return $this->state; + } +} \ No newline at end of file diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-move-product.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-move-product.php new file mode 100644 index 00000000000..3bca73ff428 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-move-product.php @@ -0,0 +1,44 @@ +state = $state; + } + + public function run( ) { + $upgrader = WC_WCCOM_Site_Installer::get_wp_upgrader(); + + $destination = 'plugin' === $this->state->get_product_type() + ? WP_PLUGIN_DIR + : get_theme_root(); + + $package = array( + 'source' => $this->state->get_unpacked_path(), + 'destination' => $destination, + 'clear_working' => true, + 'hook_extra' => array( + 'type' => $this->state->get_product_type(), + 'action' => 'install', + ), + ); + + $result = $upgrader->install_package( $package ); + + /** + * If install package returns error 'folder_exists' treat as success. + */ + if ( is_wp_error( $result ) && array_key_exists( 'folder_exists', $result->errors ) ) { + $existing_folder_path = $result->error_data[ 'folder_exists' ]; + $plugin_info = WC_WCCOM_Site_Installer::get_plugin_info( $existing_folder_path ); + + $this->state->set_installed_path( $existing_folder_path ); + $this->state->set_already_installed_plugin_info( $plugin_info ); + return $this->state; + } + + $this->state->set_installed_path( $result['destination'] ); + return $this->state; + } +} \ No newline at end of file diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-product-info.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-product-info.php new file mode 100644 index 00000000000..90ffaff9c5f --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-product-info.php @@ -0,0 +1,81 @@ +state = $state; + } + public function run() { + + $product_id = $this->state->get_product_id(); + + // Get product info from woocommerce.com. + $request = WC_Helper_API::get( + add_query_arg( + [ 'product_id' => $product_id ], + 'info' + ), + [ + 'authenticated' => true, + ] + ); + + if ( 200 !== wp_remote_retrieve_response_code( $request ) ) { + throw new Installer_Error( Installer_Error_Codes::FAILED_GETTING_PRODUCT_INFO ); + } + + $result = json_decode( wp_remote_retrieve_body( $request ), true ); + + if (!isset($result['_product_type'], $result['name'])) { + throw new Installer_Error( Installer_Error_Codes::INVALID_PRODUCT_INFO_RESPONSE ); + } + + + if ( ! empty( $result['_wporg_product'] )) { + $download_url = $this->get_wporg_download_url( $result ); + } else { + $download_url = $this->get_wccom_download_url( $product_id ); + } + + $this->state->set_product_type( $result['_product_type'] ); + $this->state->set_product_name ($result['name'] ); + $this->state->set_download_url ($download_url ); + + return $this->state; + } + + + protected function get_wporg_download_url ( $data ) { + if ( empty( $data['_wporg_product'] ) ) { + return null; + } + + if( empty( $data['download_link'] ) ) { + throw new Installer_Error( Installer_Error_Codes::WPORG_PRODUCT_MISSING_DOWNLOAD_LINK ); + } + + return $data['download_link']; + } + + protected function get_wccom_download_url( $product_id ) { + WC_Helper::_flush_subscriptions_cache(); + + if ( ! WC_Helper::has_product_subscription( $product_id ) ) { + throw new Installer_Error( Installer_Error_Codes::WCCOM_PRODUCT_MISSING_SUBSCRIPTION ); + } + + // Retrieve download URL for non-wporg product. + WC_Helper_Updater::flush_updates_cache(); + $updates = WC_Helper_Updater::get_update_data(); + + if ( empty( $updates[ $product_id ]['package'] ) ) { + return new Installer_Error( Installer_Error_Codes::WCCOM_PRODUCT_MISSING_PACKAGE ); + } + + return $updates[ $product_id ]['package']; + } +} \ No newline at end of file diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-unpack-product.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-unpack-product.php new file mode 100644 index 00000000000..773d166cc34 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-installation-step-unpack-product.php @@ -0,0 +1,26 @@ +state = $state; + } + + public function run() { + $upgrader = WC_WCCOM_Site_Installer::get_wp_upgrader(); + $unpacked_path = $upgrader->unpack_package ($this->state->get_download_path(), true ); + + if ( empty( $unpacked_path) ) { + return new Installer_Error( Installer_Error_Codes::MISSING_UNPACKED_PATH); + } + + $this->state->set_unpacked_path($unpacked_path); + + return $this->state; + + } +} \ No newline at end of file diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/interface-installaton-step.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/interface-installaton-step.php new file mode 100644 index 00000000000..b5349b26e13 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/interface-installaton-step.php @@ -0,0 +1,9 @@ + 'Authentication required', + self::NO_ACCESS_TOKEN => 'No access token provided', + self::NO_SIGNATURE => 'No signature provided', + self::SITE_NOT_CONNECTED => 'Site not connected to WooCommerce.com', + self::INVALID_TOKEN => 'Invalid access token provided', + self::REQUEST_VERIFICATION_FAILED => 'Request verification by signature failed', + self::USER_NOT_FOUND => 'Token owning user not found', + self::NO_PERMISSION => 'You do not have permission to install plugin or theme', + self::IDEMPOTENCY_KEY_MISMATCH => 'Idempotency key mismatch', + self::NO_INITIATED_INSTALLATION_FOUND => 'No initiated installation for the product found', + self::ALL_INSTALLATION_STEPS_RUN => 'All installation steps have been run', + self::REQUESTED_STEP_ALREADY_RUN => 'Requested step has already been run', + self::PLUGIN_ALREADY_INSTALLED => 'The plugin has already been installed', + self::INSTALLATION_ALREADY_RUNNING => 'The installation of the plugin is already running', + self::INSTALLATION_FAILED => 'The installation of the plugin failed', + self::FILESYSTEM_REQUIREMENTS_NOT_MET => 'The filesystem requirements are not met', + self::FAILED_GETTING_PRODUCT_INFO => 'Failed to retrieve product info from woocommerce.com', + self::INVALID_PRODUCT_INFO_RESPONSE => 'Invalid product info response from woocommerce.com', + self::WCCOM_PRODUCT_MISSING_SUBSCRIPTION => 'Product subscription is missing', + self::WCCOM_PRODUCT_MISSING_PACKAGE => 'Could not find product package', + self::MISSING_DOWNLOAD_PATH => 'Download path is missing', + self::MISSING_UNPACKED_PATH => 'Unpacked path is missing', + self::UNKNOWN_FILENAME => 'Unknown product filename', + self::PLUGIN_ACTIVATION_ERROR => 'Plugin activation error', + self::UNEXPECTED_ERROR => 'Unexpected error', + self::FAILED_TO_RESET_INSTALLATION_STATE => 'Failed to reset installation state', + ]; + + const HTTP_CODES = [ + self::NOT_AUTHENTICATED => 401, + self::NO_ACCESS_TOKEN => 400, + self::NO_SIGNATURE => 400, + self::SITE_NOT_CONNECTED => 401, + self::INVALID_TOKEN => 401, + self::REQUEST_VERIFICATION_FAILED => 400, + self::USER_NOT_FOUND => 401, + self::NO_PERMISSION => 403, + self::IDEMPOTENCY_KEY_MISMATCH => 400, + self::NO_INITIATED_INSTALLATION_FOUND => 400, + self::ALL_INSTALLATION_STEPS_RUN => 400, + self::REQUESTED_STEP_ALREADY_RUN => 400, + self::UNEXPECTED_ERROR => 500, + ]; +} diff --git a/plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-error.php b/plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-error.php new file mode 100644 index 00000000000..4ccb8ff3b86 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-error.php @@ -0,0 +1,36 @@ +error_code = $error_code; + $this->error_message = $error_message ?? WC_REST_WCCOM_Site_Installer_Error_Codes::ERROR_MESSAGES[ $error_code ] ?? ''; + $this->http_code = $http_code ?? WC_REST_WCCOM_Site_Installer_Error_Codes::HTTP_CODES[ $error_code ] ?? 400; + + parent::__construct( $error_code ); + } + + public function get_error_code( ) { + return $this->error_code; + } + + public function get_error_message( ) { + return $this->error_message; + } + + public function get_http_code( ) { + return $this->http_code; + } +} diff --git a/plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php b/plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php deleted file mode 100644 index 366b9d071b0..00000000000 --- a/plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-errors.php +++ /dev/null @@ -1,73 +0,0 @@ -namespace, + '/' . $this->rest_base, + [ + [ + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'install' ], + 'permission_callback' => [ $this, 'check_permission' ], + 'args' => [ + 'product-id' => [ + 'required' => true, + 'type' => 'integer', + ], + 'run-until-step' => [ + 'required' => true, + 'type' => 'string', + 'enum' => WC_WCCOM_Site_Installation_Manager::STEPS, + ], + 'idempotency-key' => [ + 'required' => true, + 'type' => 'string', + ], + ], + ], + [ + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => [ $this, 'reset_install' ], + 'permission_callback' => [ $this, 'check_permission' ], + 'args' => [ + 'product-id' => [ + 'required' => true, + 'type' => 'integer', + ], + 'idempotency-key' => [ + 'required' => true, + 'type' => 'string', + ], + ], + ], + ] + ); + } + + /** + * Check permissions. + * + * @since 7.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function check_permission( $request ) { + $current_user = wp_get_current_user(); + + if ( empty( $current_user ) || ( $current_user instanceof WP_User && ! $current_user->exists() ) ) { + $error = apply_filters( + WC_WCCOM_Site::AUTH_ERROR_FILTER_NAME, + new Installer_Error( Installer_Error_Codes::NOT_AUTHENTICATED ) + ); + return new WP_Error( + $error->get_error_code(), + $error->get_error_message(), + array( 'status' => $error->get_http_code() ) + ); + } + + if ( ! user_can( $current_user, 'install_plugins' ) || ! user_can( $current_user, 'install_themes' ) ) { + $error = new Installer_Error( Installer_Error_Codes::NO_PERMISSION ); + return new WP_Error( + $error->get_error_code(), + $error->get_error_message(), + array( 'status' => $error->get_http_code() ) + ); + } + + return true; + } + + /** + * Install WooCommerce.com products. + * + * @since 7.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return bool|WP_Error + */ + public function install( $request ) { + + try { + $product_id = $request['product-id']; + $run_until_step = $request['run-until-step']; + $idempotency_key = $request['idempotency-key']; + + $installation_manager = new WC_WCCOM_Site_Installation_Manager( $product_id, $idempotency_key ); + $installation_manager->run_installation( $run_until_step ); + + $response = $this->success_response($product_id); + + } catch (Installer_Error $exception) { + $response = $this->failure_response($product_id, $exception); + } + + return $response; + } + + /** + * Reset installation state. + * + * @since 7.7.0 + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error + */ + public function reset_install( $request ) { + try { + $product_id = $request['product-id']; + $idempotency_key = $request['idempotency-key']; + + $installation_manager = new WC_WCCOM_Site_Installation_Manager($product_id, $idempotency_key); + $installation_manager->reset_installation(); + + $response = $this->success_response($product_id); + + } catch (Installer_Error $exception) { + $response = $this->failure_response($product_id, $exception); + } + + return $response; + } + + protected function success_response( $product_id ) { + $state = WC_WCCOM_Site_Installation_State_Storage::get_state( $product_id ); + $response = rest_ensure_response([ + 'success' => true, + 'state' => $state ? $this->map_state_to_response( $state ) : null, + ]); + $response->set_status(200); + return $response; + } + + protected function failure_response( $product_id, $exception ) { + $state = WC_WCCOM_Site_Installation_State_Storage::get_state( $product_id ); + $response = rest_ensure_response([ + 'success' => false, + 'error_code' => $exception->get_error_code(), + 'error_message' => $exception->get_error_message(), + 'state' => $state ? $this->map_state_to_response( $state ) : null, + ]); + $response->set_status( $exception->get_http_code() ); + return $response; + } + + protected function map_state_to_response( $state ) { + return [ + 'product_id' => $state->get_product_id(), + 'idempotency_key' => $state->get_idempotency_key(), + 'last_step_name' => $state->get_last_step_name(), + 'last_step_status' => $state->get_last_step_status(), + 'last_step_error' => $state->get_last_step_error(), + 'product_type' => $state->get_product_type(), + 'product_name' => $state->get_product_name(), + 'already_installed_plugin_info' => $state->get_already_installed_plugin_info(), + 'started_seconds_ago' => time() - $state->get_started_date(), + ]; + } +} + diff --git a/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php b/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php index 03daf78cc82..f41e179a077 100644 --- a/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php +++ b/plugins/woocommerce/includes/wccom-site/rest-api/endpoints/class-wc-rest-wccom-site-installer-controller.php @@ -8,6 +8,9 @@ * @since 3.7.0 */ +use WC_REST_WCCOM_Site_Installer_Error_Codes as Installer_Error_Codes; +use WC_REST_WCCOM_Site_Installer_Error as Installer_Error; + defined( 'ABSPATH' ) || exit; /** @@ -73,30 +76,32 @@ class WC_REST_WCCOM_Site_Installer_Controller extends WC_REST_Controller { * @param WP_REST_Request $request Full details about the request. * @return bool|WP_Error */ - public function check_permission( $request ) { - $current_user = wp_get_current_user(); + public function check_permission( $request ) { + $current_user = wp_get_current_user(); - if ( empty( $current_user ) || ( $current_user instanceof WP_User && ! $current_user->exists() ) ) { - return apply_filters( - WC_WCCOM_Site::AUTH_ERROR_FILTER_NAME, - new WP_Error( - WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_CODE, - WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_MESSAGE, - array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NOT_AUTHENTICATED_HTTP_CODE ) - ) - ); - } + if ( empty( $current_user ) || ( $current_user instanceof WP_User && ! $current_user->exists() ) ) { + $error = apply_filters( + WC_WCCOM_Site::AUTH_ERROR_FILTER_NAME, + new Installer_Error( Installer_Error_Codes::NOT_AUTHENTICATED ) + ); + return new WP_Error( + $error->get_error_code(), + $error->get_error_message(), + array( 'status' => $error->get_http_code() ) + ); + } - if ( ! user_can( $current_user, 'install_plugins' ) || ! user_can( $current_user, 'install_themes' ) ) { - return new WP_Error( - WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_CODE, - WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_MESSAGE, - array( 'status' => WC_REST_WCCOM_Site_Installer_Errors::NO_PERMISSION_HTTP_CODE ) - ); - } + if ( ! user_can( $current_user, 'install_plugins' ) || ! user_can( $current_user, 'install_themes' ) ) { + $error = new Installer_Error( Installer_Error_Codes::NO_PERMISSION ); + return new WP_Error( + $error->get_error_code(), + $error->get_error_message(), + array( 'status' => $error->get_http_code() ) + ); + } - return true; - } + return true; + } /** * Get installation state.