diff --git a/plugins/woocommerce/changelog/add-wccom-installer-version-2 b/plugins/woocommerce/changelog/add-wccom-installer-version-2 new file mode 100644 index 00000000000..72aef2f644f --- /dev/null +++ b/plugins/woocommerce/changelog/add-wccom-installer-version-2 @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add plugin installer version independent of WP cron. \ No newline at end of file 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..082c53f7848 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,13 @@ class WC_WCCOM_Site_Installer { 'activate_product', ); + /** + * An instance of the WP_Upgrader class to be used for installation. + * + * @var \WP_Upgrader $wp_upgrader + */ + private static $wp_upgrader; + /** * Get the product install state. * @@ -150,15 +157,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 +423,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 +519,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 +545,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 +573,27 @@ class WC_WCCOM_Site_Installer { } return false; } + + /** + * Get an instance of WP_Upgrader to use for installing plugins. + * + * @return WP_Upgrader + */ + 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..9835b3bbb5b 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/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-wc-wccom-site-installation-step-get-product-info.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-download-product.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-unpack-product.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-move-product.php'; + require_once WC_ABSPATH . 'includes/wccom-site/installation/installation-steps/class-wc-wccom-site-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..76d49822c74 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-manager.php @@ -0,0 +1,208 @@ +product_id = $product_id; + $this->idempotency_key = $idempotency_key; + } + + /** + * Run the installation. + * + * @param string $run_until_step The step to run until. + * @return bool + * @throws WC_REST_WCCOM_Site_Installer_Error If installation failed to run. + */ + 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; + } + + /** + * Get the next step to run. + * + * @return bool + * @throws WC_REST_WCCOM_Site_Installer_Error If the installation cannot be rest. + */ + 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; + } + + /** + * Check if the installation can be run. + * + * @param string $run_until_step Run until this step. + * @param WC_WCCOM_Site_Installation_State $state Installation state. + * @return void + * @throws WC_REST_WCCOM_Site_Installer_Error If the installation cannot be run. + */ + protected function can_run_installation( $run_until_step, $state ) { + + 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, true ) >= array_search( + $run_until_step, + self::STEPS, + true + ) ) { + 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 ); + } + } + + /** + * Get the next step to run. + * + * @param WC_WCCOM_Site_Installation_State $state Installation state. + * @return string + */ + 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, true ); + + return self::STEPS[ $last_executed_step_index + 1 ]; + } + + /** + * Get the steps to run. + * + * @param string $start_step The step to start from. + * @param string $end_step The step to end at. + * @return string[] + */ + protected function get_installation_steps( string $start_step, string $end_step ) { + $start_step_offset = array_search( $start_step, self::STEPS, true ); + $end_step_index = array_search( $end_step, self::STEPS, true ); + $length = $end_step_index - $start_step_offset + 1; + + return array_slice( self::STEPS, $start_step_offset, $length ); + } + + /** + * Run the step. + * + * @param string $step_name Step name. + * @param WC_WCCOM_Site_Installation_State $state Installation state. + * @return void + * @throws WC_REST_WCCOM_Site_Installer_Error If the step fails. + */ + 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..0cc0eac65d3 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-state-storage.php @@ -0,0 +1,100 @@ +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; + } + + /** + * Save state to storage. + * + * @param WC_WCCOM_Site_Installation_State $state The state to save. + * @return bool + */ + 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, + array( + '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(), + ) + ); + } + + /** + * Delete state from storage. + * + * @param WC_WCCOM_Site_Installation_State $state The state to delete. + * @return bool + */ + 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 ); + } + + /** + * Get the storage key for a product ID. + * + * @param int $product_id The product ID. + * @return string + */ + 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..3a9db09fbef --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/class-wc-wccom-site-installation-state.php @@ -0,0 +1,343 @@ +product_id = $product_id; + } + + /** + * Initiate an existing installation state. + * + * @param int $product_id The product ID. + * @param string $idempotency_key The idempotency key. + * @param string $last_step_name The last step name. + * @param string $last_step_status The last step status. + * @param string $last_step_error The last step error. + * @param int $started_date The timestamp of the installation start. + * @return WC_WCCOM_Site_Installation_State The instance. + */ + 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; + } + + /** + * Initiate a new installation state. + * + * @param init $product_id The product ID. + * @param string $idempotency_key The idempotency key. + * @return WC_WCCOM_Site_Installation_State The 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; + } + + /** + * Get the product ID. + * + * @return string + */ + public function get_product_id() { + return $this->product_id; + } + + /** + * Get the idempotency key. + * + * @return string + */ + public function get_idempotency_key() { + return $this->idempotency_key; + } + + /** + * Get the timestamp of the installation start. + * + * @return int + */ + public function get_last_step_name() { + return $this->last_step_name; + } + + /** + * Get the last step status. + * + * @return string + */ + public function get_last_step_status() { + return $this->last_step_status; + } + + /** + * Get the last step error. + * + * @return int + */ + public function get_last_step_error() { + return $this->last_step_error; + } + + /** + * Initiate a step. + * + * @param string $step_name Step name. + * @return void + */ + public function initiate_step( $step_name ) { + $this->last_step_name = $step_name; + $this->last_step_status = self::STEP_STATUS_IN_PROGRESS; + } + + /** + * Capture a successful installation of a step. + * + * @param string $step_name The step name. + */ + public function complete_step( $step_name ) { + $this->last_step_name = $step_name; + $this->last_step_status = self::STEP_STATUS_COMPLETED; + } + + /** + * Capture an installation failure. + * + * @param string $step_name The step name. + * @param string $error_code The error code. + */ + 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; + } + + /** + * Get the product type. + * + * @return string + */ + public function get_product_type() { + return $this->product_type; + } + + /** + * Set the product type. + * + * @param string $product_type The product type. + */ + public function set_product_type( $product_type ) { + $this->product_type = $product_type; + } + /** + * Get the product name. + * + * @return string + */ + public function get_product_name() { + return $this->product_name; + } + /** + * Set the product name. + * + * @param string $product_name The product name. + */ + public function set_product_name( $product_name ) { + $this->product_name = $product_name; + } + /** + * Get the download URL. + * + * @return string + */ + public function get_download_url() { + return $this->download_url; + } + /** + * Set the download URL. + * + * @param string $download_url The download URL. + */ + public function set_download_url( $download_url ) { + $this->download_url = $download_url; + } + /** + * Get the path to the downloaded file. + * + * @return string + */ + public function get_download_path() { + return $this->download_path; + } + /** + * Set the path to the downloaded file. + * + * @param string $download_path The path to the downloaded file. + */ + public function set_download_path( $download_path ) { + $this->download_path = $download_path; + } + /** + * Get the path to the unpacked file. + * + * @return string + */ + public function get_unpacked_path() { + return $this->unpacked_path; + } + /** + * Set the path to the unpacked file. + * + * @param string $unpacked_path The path to the unpacked file. + */ + public function set_unpacked_path( $unpacked_path ) { + $this->unpacked_path = $unpacked_path; + } + /** + * Get the path to the installed file. + * + * @return string + */ + public function get_installed_path() { + return $this->installed_path; + } + /** + * Set the path to the installed file. + * + * @param string $installed_path The path to the installed file. + */ + public function set_installed_path( $installed_path ) { + $this->installed_path = $installed_path; + } + /** + * Get the plugin info for the already installed plugin. + * + * @return array + */ + public function get_already_installed_plugin_info() { + return $this->already_installed_plugin_info; + } + /** + * Set the plugin info for the already installed plugin. + * + * @param array $plugin_info The plugin info. + */ + public function set_already_installed_plugin_info( $plugin_info ) { + $this->already_installed_plugin_info = $plugin_info; + } + /** + * Get the timestamp of the installation start. + * + * @return int + */ + public function get_started_date() { + return $this->started_date; + } +} diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-activate-product.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-activate-product.php new file mode 100644 index 00000000000..d9123348d0e --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-activate-product.php @@ -0,0 +1,140 @@ +state = $state; + } + + /** + * Run the step installation process. + */ + 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; + } + + /** + * Activate plugin. + * + * @param int $product_id Product ID. + */ + 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(), + array( + '_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() ); + } + } + + /** + * Activate theme. + * + * @param int $product_id Product ID. + */ + 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(), + array( + '_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 ); + } + + /** + * Get WP.org product directory name. + * + * @return string|false + */ + 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() ); + } +} diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-download-product.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-download-product.php new file mode 100644 index 00000000000..3ff0e88fbed --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-download-product.php @@ -0,0 +1,52 @@ +state = $state; + } + + /** + * Run the step installation process. + * + * @throws Installer_Error Installer Error. + */ + 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; + } +} diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-get-product-info.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-get-product-info.php new file mode 100644 index 00000000000..8d819bca142 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-get-product-info.php @@ -0,0 +1,122 @@ +state = $state; + } + + /** + * Run the step installation process. + * + * @throws Installer_Error Installer Error. + * @return array + */ + public function run() { + $product_id = $this->state->get_product_id(); + + // Get product info from woocommerce.com. + $request = WC_Helper_API::get( + add_query_arg( + array( 'product_id' => $product_id ), + 'info' + ), + array( + '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; + } + + /** + * Get download URL for wporg product. + * + * @param array $data Product data. + * + * @return string|null + * @throws Installer_Error Installer Error. + */ + 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']; + } + + /** + * Get download URL for wccom product. + * + * @param int $product_id Product ID. + * + * @return string + * @throws Installer_Error Installer Error. + */ + 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']; + } +} diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-move-product.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-move-product.php new file mode 100644 index 00000000000..09018fb764b --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-move-product.php @@ -0,0 +1,70 @@ +state = $state; + } + + /** + * Run the step installation process. + */ + 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; + } +} diff --git a/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-unpack-product.php b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-unpack-product.php new file mode 100644 index 00000000000..69bdecb4134 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/class-wc-wccom-site-installation-step-unpack-product.php @@ -0,0 +1,49 @@ +state = $state; + } + + /** + * Run the step installation process. + */ + 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; + } +} 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..4fe036a1e50 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/installation/installation-steps/interface-installaton-step.php @@ -0,0 +1,23 @@ + '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 = array( + 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..fdc1bc37129 --- /dev/null +++ b/plugins/woocommerce/includes/wccom-site/rest-api/class-wc-rest-wccom-site-installer-error.php @@ -0,0 +1,51 @@ +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 ); + } + + /** + * Get the error code. + */ + public function get_error_code() { + return $this->error_code; + } + + /** + * Get the error message. + */ + public function get_error_message() { + return $this->error_message; + } + + /** + * Get the HTTP status code. + */ + 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, + array( + array( + 'methods' => WP_REST_Server::EDITABLE, + 'callback' => array( $this, 'install' ), + 'permission_callback' => array( $this, 'check_permission' ), + 'args' => array( + 'product-id' => array( + 'required' => true, + 'type' => 'integer', + ), + 'run-until-step' => array( + 'required' => true, + 'type' => 'string', + 'enum' => WC_WCCOM_Site_Installation_Manager::STEPS, + ), + 'idempotency-key' => array( + 'required' => true, + 'type' => 'string', + ), + ), + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => array( $this, 'reset_install' ), + 'permission_callback' => array( $this, 'check_permission' ), + 'args' => array( + 'product-id' => array( + 'required' => true, + 'type' => 'integer', + ), + 'idempotency-key' => array( + '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() ) ) { + /** + * This filter allows to provide a custom error message when the user is not authenticated. + * + * @since 3.7.0 + */ + $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 WP_REST_Response|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; + } + + /** + * Generate a standardized response for a successful request. + * + * @param int $product_id Product ID. + * @return WP_REST_Response|WP_Error + */ + protected function success_response( $product_id ) { + $state = WC_WCCOM_Site_Installation_State_Storage::get_state( $product_id ); + $response = rest_ensure_response( + array( + 'success' => true, + 'state' => $state ? $this->map_state_to_response( $state ) : null, + ) + ); + $response->set_status( 200 ); + return $response; + } + + /** + * Generate a standardized response for a failed request. + * + * @param int $product_id Product ID. + * @param Installer_Error $exception The exception. + * @return WP_REST_Response|WP_Error + */ + protected function failure_response( $product_id, $exception ) { + $state = WC_WCCOM_Site_Installation_State_Storage::get_state( $product_id ); + $response = rest_ensure_response( + array( + '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; + } + + /** + * Map the installation state to a response. + * + * @param WC_WCCOM_Site_Installation_State $state The installation state. + * @return array + */ + protected function map_state_to_response( $state ) { + return array( + '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..5c25390bac9 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; /** @@ -77,21 +80,28 @@ class WC_REST_WCCOM_Site_Installer_Controller extends WC_REST_Controller { $current_user = wp_get_current_user(); if ( empty( $current_user ) || ( $current_user instanceof WP_User && ! $current_user->exists() ) ) { - return apply_filters( + /** + * This filter allows to provide a custom error message when the user is not authenticated. + * + * @since 3.7.0 + */ + $error = 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 ) - ) + 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( - 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 ) + $error->get_error_code(), + $error->get_error_message(), + array( 'status' => $error->get_http_code() ) ); }