No Description

class-wc-wccom-site-installer.php 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. <?php
  2. /**
  3. * WooCommerce.com Product Installation.
  4. *
  5. * @package WooCommerce\WCCom
  6. * @since 3.7.0
  7. */
  8. defined( 'ABSPATH' ) || exit;
  9. /**
  10. * WC_WCCOM_Site_Installer Class
  11. *
  12. * Contains functionalities to install products via WooCommerce.com helper connection.
  13. */
  14. class WC_WCCOM_Site_Installer {
  15. /**
  16. * Error message returned install_package if the folder already exists.
  17. *
  18. * @var string
  19. */
  20. private static $folder_exists = 'folder_exists';
  21. /**
  22. * Default state.
  23. *
  24. * @var array
  25. */
  26. private static $default_state = array(
  27. 'status' => 'idle',
  28. 'steps' => array(),
  29. 'current_step' => null,
  30. );
  31. /**
  32. * Represents product step state.
  33. *
  34. * @var array
  35. */
  36. private static $default_step_state = array(
  37. 'download_url' => '',
  38. 'product_type' => '',
  39. 'last_step' => '',
  40. 'last_error' => '',
  41. 'download_path' => '',
  42. 'unpacked_path' => '',
  43. 'installed_path' => '',
  44. 'activate' => false,
  45. );
  46. /**
  47. * Product install steps. Each step is a method name in this class that
  48. * will be passed with product ID arg \WP_Upgrader instance.
  49. *
  50. * @var array
  51. */
  52. private static $install_steps = array(
  53. 'get_product_info',
  54. 'download_product',
  55. 'unpack_product',
  56. 'move_product',
  57. 'activate_product',
  58. );
  59. /**
  60. * Get the product install state.
  61. *
  62. * @since 3.7.0
  63. * @param string $key Key in state data. If empty key is passed array of
  64. * state will be returned.
  65. * @return array Product install state.
  66. */
  67. public static function get_state( $key = '' ) {
  68. $state = WC_Helper_Options::get( 'product_install', self::$default_state );
  69. if ( ! empty( $key ) ) {
  70. return isset( $state[ $key ] ) ? $state[ $key ] : null;
  71. }
  72. return $state;
  73. }
  74. /**
  75. * Update the product install state.
  76. *
  77. * @since 3.7.0
  78. * @param string $key Key in state data.
  79. * @param mixed $value Value.
  80. */
  81. public static function update_state( $key, $value ) {
  82. $state = WC_Helper_Options::get( 'product_install', self::$default_state );
  83. $state[ $key ] = $value;
  84. WC_Helper_Options::update( 'product_install', $state );
  85. }
  86. /**
  87. * Reset product install state.
  88. *
  89. * @since 3.7.0
  90. * @param array $products List of product IDs.
  91. */
  92. public static function reset_state( $products = array() ) {
  93. WC()->queue()->cancel_all( 'woocommerce_wccom_install_products' );
  94. WC_Helper_Options::update( 'product_install', self::$default_state );
  95. }
  96. /**
  97. * Schedule installing given list of products.
  98. *
  99. * @since 3.7.0
  100. * @param array $products Array of products where key is product ID and
  101. * element is install args.
  102. * @return array State.
  103. */
  104. public static function schedule_install( $products ) {
  105. $state = self::get_state();
  106. $status = ! empty( $state['status'] ) ? $state['status'] : '';
  107. if ( 'in-progress' === $status ) {
  108. return $state;
  109. }
  110. self::update_state( 'status', 'in-progress' );
  111. $steps = array_fill_keys( array_keys( $products ), self::$default_step_state );
  112. self::update_state( 'steps', $steps );
  113. self::update_state( 'current_step', null );
  114. $args = array(
  115. 'products' => $products,
  116. );
  117. // Clear the cache of customer's subscription before asking for them.
  118. // Thus, they will be re-fetched from WooCommerce.com after a purchase.
  119. WC_Helper::_flush_subscriptions_cache();
  120. WC()->queue()->cancel_all( 'woocommerce_wccom_install_products', $args );
  121. WC()->queue()->add( 'woocommerce_wccom_install_products', $args );
  122. return self::get_state();
  123. }
  124. /**
  125. * Install a given product IDs.
  126. *
  127. * Run via `woocommerce_wccom_install_products` hook.
  128. *
  129. * @since 3.7.0
  130. * @param array $products Array of products where key is product ID and
  131. * element is install args.
  132. */
  133. public static function install( $products ) {
  134. require_once ABSPATH . 'wp-admin/includes/file.php';
  135. require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
  136. require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
  137. require_once ABSPATH . 'wp-admin/includes/plugin.php';
  138. WP_Filesystem();
  139. $upgrader = new WP_Upgrader( new Automatic_Upgrader_Skin() );
  140. $upgrader->init();
  141. wp_clean_plugins_cache();
  142. foreach ( $products as $product_id => $install_args ) {
  143. self::install_product( $product_id, $install_args, $upgrader );
  144. }
  145. self::finish_installation();
  146. }
  147. /**
  148. * Finish installation by updating the state.
  149. *
  150. * @since 3.7.0
  151. */
  152. private static function finish_installation() {
  153. $state = self::get_state();
  154. if ( empty( $state['steps'] ) ) {
  155. return;
  156. }
  157. foreach ( $state['steps'] as $step ) {
  158. if ( ! empty( $step['last_error'] ) ) {
  159. $state['status'] = 'has_error';
  160. break;
  161. }
  162. }
  163. if ( 'has_error' !== $state['status'] ) {
  164. $state['status'] = 'finished';
  165. }
  166. WC_Helper_Options::update( 'product_install', $state );
  167. }
  168. /**
  169. * Install a single product given its ID.
  170. *
  171. * @since 3.7.0
  172. * @param int $product_id Product ID.
  173. * @param array $install_args Install args.
  174. * @param \WP_Upgrader $upgrader Core class to handle installation.
  175. */
  176. private static function install_product( $product_id, $install_args, $upgrader ) {
  177. foreach ( self::$install_steps as $step ) {
  178. self::do_install_step( $product_id, $install_args, $step, $upgrader );
  179. }
  180. }
  181. /**
  182. * Perform product installation step.
  183. *
  184. * @since 3.7.0
  185. * @param int $product_id Product ID.
  186. * @param array $install_args Install args.
  187. * @param string $step Installation step.
  188. * @param \WP_Upgrader $upgrader Core class to handle installation.
  189. */
  190. private static function do_install_step( $product_id, $install_args, $step, $upgrader ) {
  191. $state_steps = self::get_state( 'steps' );
  192. if ( empty( $state_steps[ $product_id ] ) ) {
  193. $state_steps[ $product_id ] = self::$default_step_state;
  194. }
  195. if ( ! empty( $state_steps[ $product_id ]['last_error'] ) ) {
  196. return;
  197. }
  198. $state_steps[ $product_id ]['last_step'] = $step;
  199. if ( ! empty( $install_args['activate'] ) ) {
  200. $state_steps[ $product_id ]['activate'] = true;
  201. }
  202. self::update_state(
  203. 'current_step',
  204. array(
  205. 'product_id' => $product_id,
  206. 'step' => $step,
  207. )
  208. );
  209. $result = call_user_func( array( __CLASS__, $step ), $product_id, $upgrader );
  210. if ( is_wp_error( $result ) ) {
  211. $state_steps[ $product_id ]['last_error'] = $result->get_error_message();
  212. } else {
  213. switch ( $step ) {
  214. case 'get_product_info':
  215. $state_steps[ $product_id ]['download_url'] = $result['download_url'];
  216. $state_steps[ $product_id ]['product_type'] = $result['product_type'];
  217. $state_steps[ $product_id ]['product_name'] = $result['product_name'];
  218. break;
  219. case 'download_product':
  220. $state_steps[ $product_id ]['download_path'] = $result;
  221. break;
  222. case 'unpack_product':
  223. $state_steps[ $product_id ]['unpacked_path'] = $result;
  224. break;
  225. case 'move_product':
  226. $state_steps[ $product_id ]['installed_path'] = $result['destination'];
  227. if ( isset( $result[ self::$folder_exists ] ) ) {
  228. $state_steps[ $product_id ]['warning'] = array(
  229. 'message' => self::$folder_exists,
  230. 'plugin_info' => self::get_plugin_info( $state_steps[ $product_id ]['installed_path'] ),
  231. );
  232. }
  233. break;
  234. }
  235. }
  236. self::update_state( 'steps', $state_steps );
  237. }
  238. /**
  239. * Get product info from its ID.
  240. *
  241. * @since 3.7.0
  242. * @param int $product_id Product ID.
  243. * @return array|\WP_Error
  244. */
  245. private static function get_product_info( $product_id ) {
  246. $product_info = array(
  247. 'download_url' => '',
  248. 'product_type' => '',
  249. );
  250. // Get product info from woocommerce.com.
  251. $request = WC_Helper_API::get(
  252. add_query_arg(
  253. array( 'product_id' => absint( $product_id ) ),
  254. 'info'
  255. ),
  256. array(
  257. 'authenticated' => true,
  258. )
  259. );
  260. if ( 200 !== wp_remote_retrieve_response_code( $request ) ) {
  261. return new WP_Error( 'product_info_failed', __( 'Failed to retrieve product info from woocommerce.com', 'woocommerce' ) );
  262. }
  263. $result = json_decode( wp_remote_retrieve_body( $request ), true );
  264. $product_info['product_type'] = $result['_product_type'];
  265. $product_info['product_name'] = $result['name'];
  266. if ( ! empty( $result['_wporg_product'] ) && ! empty( $result['download_link'] ) ) {
  267. // For wporg product, download is set already from info response.
  268. $product_info['download_url'] = $result['download_link'];
  269. } elseif ( ! WC_Helper::has_product_subscription( $product_id ) ) {
  270. // Non-wporg product needs subscription.
  271. return new WP_Error( 'missing_subscription', __( 'Missing product subscription', 'woocommerce' ) );
  272. } else {
  273. // Retrieve download URL for non-wporg product.
  274. WC_Helper_Updater::flush_updates_cache();
  275. $updates = WC_Helper_Updater::get_update_data();
  276. if ( empty( $updates[ $product_id ]['package'] ) ) {
  277. return new WP_Error( 'missing_product_package', __( 'Could not find product package.', 'woocommerce' ) );
  278. }
  279. $product_info['download_url'] = $updates[ $product_id ]['package'];
  280. }
  281. return $product_info;
  282. }
  283. /**
  284. * Download product by its ID and returns the path of the zip package.
  285. *
  286. * @since 3.7.0
  287. * @param int $product_id Product ID.
  288. * @param \WP_Upgrader $upgrader Core class to handle installation.
  289. * @return \WP_Error|string
  290. */
  291. private static function download_product( $product_id, $upgrader ) {
  292. $steps = self::get_state( 'steps' );
  293. if ( empty( $steps[ $product_id ]['download_url'] ) ) {
  294. return new WP_Error( 'missing_download_url', __( 'Could not find download url for the product.', 'woocommerce' ) );
  295. }
  296. return $upgrader->download_package( $steps[ $product_id ]['download_url'] );
  297. }
  298. /**
  299. * Unpack downloaded product.
  300. *
  301. * @since 3.7.0
  302. * @param int $product_id Product ID.
  303. * @param \WP_Upgrader $upgrader Core class to handle installation.
  304. * @return \WP_Error|string
  305. */
  306. private static function unpack_product( $product_id, $upgrader ) {
  307. $steps = self::get_state( 'steps' );
  308. if ( empty( $steps[ $product_id ]['download_path'] ) ) {
  309. return new WP_Error( 'missing_download_path', __( 'Could not find download path.', 'woocommerce' ) );
  310. }
  311. return $upgrader->unpack_package( $steps[ $product_id ]['download_path'], true );
  312. }
  313. /**
  314. * Move product to plugins directory.
  315. *
  316. * @since 3.7.0
  317. * @param int $product_id Product ID.
  318. * @param \WP_Upgrader $upgrader Core class to handle installation.
  319. * @return array|\WP_Error
  320. */
  321. private static function move_product( $product_id, $upgrader ) {
  322. $steps = self::get_state( 'steps' );
  323. if ( empty( $steps[ $product_id ]['unpacked_path'] ) ) {
  324. return new WP_Error( 'missing_unpacked_path', __( 'Could not find unpacked path.', 'woocommerce' ) );
  325. }
  326. $destination = 'plugin' === $steps[ $product_id ]['product_type']
  327. ? WP_PLUGIN_DIR
  328. : get_theme_root();
  329. $package = array(
  330. 'source' => $steps[ $product_id ]['unpacked_path'],
  331. 'destination' => $destination,
  332. 'clear_working' => true,
  333. 'hook_extra' => array(
  334. 'type' => $steps[ $product_id ]['product_type'],
  335. 'action' => 'install',
  336. ),
  337. );
  338. $result = $upgrader->install_package( $package );
  339. /**
  340. * If install package returns error 'folder_exists' threat as success.
  341. */
  342. if ( is_wp_error( $result ) && array_key_exists( self::$folder_exists, $result->errors ) ) {
  343. return array(
  344. self::$folder_exists => true,
  345. 'destination' => $result->error_data[ self::$folder_exists ],
  346. );
  347. }
  348. return $result;
  349. }
  350. /**
  351. * Activate product given its product ID.
  352. *
  353. * @since 3.7.0
  354. * @param int $product_id Product ID.
  355. * @return \WP_Error|null
  356. */
  357. private static function activate_product( $product_id ) {
  358. $steps = self::get_state( 'steps' );
  359. if ( ! $steps[ $product_id ]['activate'] ) {
  360. return null;
  361. }
  362. if ( 'plugin' === $steps[ $product_id ]['product_type'] ) {
  363. return self::activate_plugin( $product_id );
  364. }
  365. return self::activate_theme( $product_id );
  366. }
  367. /**
  368. * Activate plugin given its product ID.
  369. *
  370. * @since 3.7.0
  371. * @param int $product_id Product ID.
  372. * @return \WP_Error|null
  373. */
  374. private static function activate_plugin( $product_id ) {
  375. // Clear plugins cache used in `WC_Helper::get_local_woo_plugins`.
  376. wp_clean_plugins_cache();
  377. $filename = false;
  378. // If product is WP.org one, find out its filename.
  379. $dir_name = self::get_wporg_product_dir_name( $product_id );
  380. if ( false !== $dir_name ) {
  381. $filename = self::get_wporg_plugin_main_file( $dir_name );
  382. }
  383. if ( false === $filename ) {
  384. $plugins = wp_list_filter(
  385. WC_Helper::get_local_woo_plugins(),
  386. array(
  387. '_product_id' => $product_id,
  388. )
  389. );
  390. $filename = is_array( $plugins ) && ! empty( $plugins ) ? key( $plugins ) : '';
  391. }
  392. if ( empty( $filename ) ) {
  393. return new WP_Error( 'unknown_filename', __( 'Unknown product filename.', 'woocommerce' ) );
  394. }
  395. return activate_plugin( $filename );
  396. }
  397. /**
  398. * Activate theme given its product ID.
  399. *
  400. * @since 3.7.0
  401. * @param int $product_id Product ID.
  402. * @return \WP_Error|null
  403. */
  404. private static function activate_theme( $product_id ) {
  405. // Clear plugins cache used in `WC_Helper::get_local_woo_themes`.
  406. wp_clean_themes_cache();
  407. $theme_slug = false;
  408. // If product is WP.org theme, find out its slug.
  409. $dir_name = self::get_wporg_product_dir_name( $product_id );
  410. if ( false !== $dir_name ) {
  411. $theme_slug = basename( $dir_name );
  412. }
  413. if ( false === $theme_slug ) {
  414. $themes = wp_list_filter(
  415. WC_Helper::get_local_woo_themes(),
  416. array(
  417. '_product_id' => $product_id,
  418. )
  419. );
  420. $theme_slug = is_array( $themes ) && ! empty( $themes ) ? dirname( key( $themes ) ) : '';
  421. }
  422. if ( empty( $theme_slug ) ) {
  423. return new WP_Error( 'unknown_filename', __( 'Unknown product filename.', 'woocommerce' ) );
  424. }
  425. return switch_theme( $theme_slug );
  426. }
  427. /**
  428. * Get installed directory of WP.org product.
  429. *
  430. * @since 3.7.0
  431. * @param int $product_id Product ID.
  432. * @return bool|string
  433. */
  434. private static function get_wporg_product_dir_name( $product_id ) {
  435. $steps = self::get_state( 'steps' );
  436. $product = $steps[ $product_id ];
  437. if ( empty( $product['download_url'] ) || empty( $product['installed_path'] ) ) {
  438. return false;
  439. }
  440. // Check whether product was downloaded from WordPress.org.
  441. $parsed_url = wp_parse_url( $product['download_url'] );
  442. if ( ! empty( $parsed_url['host'] ) && 'downloads.wordpress.org' !== $parsed_url['host'] ) {
  443. return false;
  444. }
  445. return basename( $product['installed_path'] );
  446. }
  447. /**
  448. * Get WP.org plugin's main file.
  449. *
  450. * @since 3.7.0
  451. * @param string $dir Directory name of the plugin.
  452. * @return bool|string
  453. */
  454. private static function get_wporg_plugin_main_file( $dir ) {
  455. // Ensure that exact dir name is used.
  456. $dir = trailingslashit( $dir );
  457. if ( ! function_exists( 'get_plugins' ) ) {
  458. require_once ABSPATH . 'wp-admin/includes/plugin.php';
  459. }
  460. $plugins = get_plugins();
  461. foreach ( $plugins as $path => $plugin ) {
  462. if ( 0 === strpos( $path, $dir ) ) {
  463. return $path;
  464. }
  465. }
  466. return false;
  467. }
  468. /**
  469. * Get plugin info
  470. *
  471. * @since 3.9.0
  472. * @param string $dir Directory name of the plugin.
  473. * @return bool|array
  474. */
  475. private static function get_plugin_info( $dir ) {
  476. $plugin_folder = basename( $dir );
  477. if ( ! function_exists( 'get_plugins' ) ) {
  478. require_once ABSPATH . 'wp-admin/includes/plugin.php';
  479. }
  480. $plugins = get_plugins();
  481. $related_plugins = array_filter(
  482. $plugins,
  483. function( $key ) use ( $plugin_folder ) {
  484. return strpos( $key, $plugin_folder . '/' ) === 0;
  485. },
  486. ARRAY_FILTER_USE_KEY
  487. );
  488. if ( 1 === count( $related_plugins ) ) {
  489. $plugin_key = array_keys( $related_plugins )[0];
  490. $plugin_data = $plugins[ $plugin_key ];
  491. return array(
  492. 'name' => $plugin_data['Name'],
  493. 'version' => $plugin_data['Version'],
  494. 'active' => is_plugin_active( $plugin_key ),
  495. );
  496. }
  497. return false;
  498. }
  499. }