Completed
Push — master ( e7c0c8...2abd1e )
by
unknown
08:58 queued 10s
created

WC_WCCOM_Site_Installer::activate_theme()   B

Complexity

Conditions 6
Paths 20

Size

Total Lines 28

Duplication

Lines 28
Ratio 100 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
cc 6
nc 20
nop 1
dl 28
loc 28
ccs 0
cts 14
cp 0
crap 42
rs 8.8497
c 0
b 0
f 0
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
Unused Code introduced by
The parameter $products is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
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
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
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
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Duplication introduced by
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
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
Duplication introduced by
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
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
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
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
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
Duplication introduced by
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
Duplication introduced by
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