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