WordPointsOrg_Module_Upgrader   C
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 625
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 7
Bugs 2 Features 0
Metric Value
dl 0
loc 625
rs 5.7474
c 7
b 2
f 0
wmc 62
lcom 1
cbo 1

15 Methods

Rating   Name   Duplication   Size   Complexity  
A _before_upgrade() 0 17 2
B _upgrade() 0 58 6
B _after_upgrade() 0 29 4
B maybe_start_maintenance_mode() 0 22 6
A _bail_early() 0 15 2
A upgrade_strings() 0 18 1
A install_strings() 0 14 1
B install() 0 39 5
A upgrade() 0 12 3
B bulk_upgrade() 0 46 5
B check_package() 0 41 6
A module_info() 0 14 4
A deactivate_module_before_upgrade() 0 18 4
B correct_module_dir_name() 0 29 6
C delete_old_module() 0 37 7

How to fix   Complexity   

Complex Class

Complex classes like WordPointsOrg_Module_Upgrader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use WordPointsOrg_Module_Upgrader, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * WordPoints.org module upgrader class.
5
 *
6
 * @package WordPointsOrg
7
 * @since 1.0.0
8
 */
9
10
/**
11
 * The WordPress upgreaders.
12
 *
13
 * @since 1.0.0
14
 */
15
include_once( ABSPATH . '/wp-admin/includes/class-wp-upgrader.php' );
16
17
/**
18
 * The WordPoints module installer class.
19
 *
20
 * @since 1.0.0
21
 */
22
include_once( WORDPOINTS_DIR . '/admin/includes/class-wordpoints-module-installer.php' );
23
24
/**
25
 * Clean the WordPoints modules cache.
26
 *
27
 * @since 1.0.0
28
 *
29
 * @param bool $clear_update_cache Whether to clear the updates cache.
30
 */
31
function wordpointsorg_clean_modules_cache( $clear_update_cache = true ) {
32
33
	if ( $clear_update_cache ) {
34
		delete_site_transient( 'wordpoints_module_updates' );
35
	}
36
37
	wp_cache_delete( 'wordpoints_modules', 'wordpoints_modules' );
38
}
39
40
/**
41
 * WordPoints.org module upgrader class.
42
 *
43
 * This class is based on the WordPress Plugin_Upgrader class, and is designed to
44
 * upgrade/install modules from a local zip, remote zip URL, or uploaded zip file.
45
 *
46
 * @see WP_Upgrader The WP Upgrader class.
47
 *
48
 * @since 1.0.0
49
 */
50
final class WordPointsOrg_Module_Upgrader extends WordPoints_Module_Installer {
51
52
	//
53
	// Public Vars.
54
	//
55
56
	/**
57
	 * Whether we are performing a bulk upgrade.
58
	 *
59
	 * @since 1.0.0
60
	 *
61
	 * @type bool $bulk
62
	 */
63
	public $bulk = false;
64
65
	/**
66
	 * Whether the upgrade routine bailed out early.
67
	 *
68
	 * @since 1.0.0
69
	 *
70
	 * @var bool
71
	 */
72
	public $bailed_early = false;
73
74
	//
75
	// Private Methods.
76
	//
77
78
	/**
79
	 * Set up the strings for a module upgrade.
80
	 *
81
	 * @since 1.0.0
82
	 */
83
	private function upgrade_strings() {
84
85
		$upgrade_strings = array(
86
			'up_to_date'          => __( 'The module is at the latest version.', 'wordpointsorg' ),
87
			'no_package'          => __( 'Update package not available.', 'wordpointsorg' ),
88
			'no_channel'          => __( 'That module cannot be updated, because there is no channel specified to receive updates through.', 'wordpointsorg' ),
89
			'api_not_found'       => __( 'That module cannot be updated, because there is no API installed that can communicate with that channel.', 'wordpointsorg' ),
90
			'downloading_package' => sprintf( __( 'Downloading update from %s&#8230;', 'wordpointsorg' ), '<span class="code">%s</span>' ),
91
			'unpack_package'      => __( 'Unpacking the update&#8230;', 'wordpointsorg' ),
92
			'remove_old'          => __( 'Removing the old version of the module&#8230;', 'wordpointsorg' ),
93
			'remove_old_failed'   => __( 'Could not remove the old module.', 'wordpointsorg' ),
94
			'process_failed'      => __( 'Module update failed.', 'wordpointsorg' ),
95
			'process_success'     => __( 'Module updated successfully.', 'wordpointsorg' ),
96
			'not_installed'       => __( 'That module cannot be updated, because it is not installed.', 'wordpointsorg' ),
97
		);
98
99
		$this->strings = array_merge( $this->strings, $upgrade_strings );
100
	}
101
102
	/**
103
	 * Set up the strings for a module install.
104
	 *
105
	 * @since 1.0.0
106
	 */
107
	private function install_strings() {
108
109
		$install_strings = array(
110
			'no_package'          => __( 'Install package not available.', 'wordpointsorg' ),
111
			'downloading_package' => sprintf( __( 'Downloading install package from %s&#8230;', 'wordpointsorg' ), '<span class="code">%s</span>' ),
112
			'unpack_package'      => __( 'Unpacking the package&#8230;', 'wordpointsorg' ),
113
			'installing_package'  => __( 'Installing the module&#8230;', 'wordpointsorg' ),
114
			'no_files'            => __( 'The module contains no files.', 'wordpointsorg' ),
115
			'process_failed'      => __( 'Module install failed.', 'wordpointsorg' ),
116
			'process_success'     => __( 'Module installed successfully.', 'wordpointsorg' ),
117
		);
118
119
		$this->strings = array_merge( $this->strings, $install_strings );
120
	}
121
122
	//
123
	// Public Methods.
124
	//
125
126
	/**
127
	 * Install a module.
128
	 *
129
	 * @since 1.0.0
130
	 *
131
	 * @param string $package URL of the zip package of the module source.
132
	 * @param array  $args    {
133
	 *        Optional arguments.
134
	 *
135
	 *        @type bool $clear_update_cache Whether the to clear the update cache.
136
	 *                                       The default is true.
137
	 * }
138
	 *
139
	 * @return bool|WP_Error True on success, false or a WP_Error on failure.
140
	 */
141
	public function install( $package, $args = array() ) {
142
143
		$args = wp_parse_args( $args, array( 'clear_update_cache' => true ) );
144
145
		$this->init();
146
		$this->install_strings();
147
148
		add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
149
150
		$result = $this->run(
151
			array(
152
				'package'           => $package,
153
				'destination'       => wordpoints_modules_dir(),
154
				'clear_destination' => false,
155
				'clear_working'     => true,
156
				'hook_extra'        => array(),
157
			)
158
		);
159
160
		remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
161
162
		if ( ! $result || is_wp_error( $result ) ) {
163
			return $result;
164
		}
165
166
		if ( ! $this->result || is_wp_error( $this->result ) ) {
167
			return $this->result;
168
		}
169
170
		// Force refresh of module update cache.
171
		wordpointsorg_clean_modules_cache( $args['clear_update_cache'] );
172
173
		/**
174
		 * This action is documented in /wp-admin/includes/class-wp-upgrader.php.
175
		 */
176
		do_action( 'upgrader_process_complete', $this, array( 'action' => 'install', 'type' => 'wordpoints_module' ), $package );
177
178
		return true;
179
	}
180
181
	/**
182
	 * Upgrade a module.
183
	 *
184
	 * @since 1.0.0
185
	 *
186
	 * @param string $module_file Basename path to the module file.
187
	 * @param array  $args        {
188
	 *        Optional arguments.
189
	 *
190
	 *        @type bool $clear_update_cache Whether the to clear the update cache.
191
	 *                                       The default is true.
192
	 * }
193
	 *
194
	 * @return bool|WP_Error True on success, false or a WP_Error on failure.
195
	 */
196
	public function upgrade( $module_file, $args = array() ) {
197
198
		$args = $this->_before_upgrade( $args );
199
		$result = $this->_upgrade( $module_file );
200
		$this->_after_upgrade( $module_file, $args );
201
202
		if ( ! $result || is_wp_error( $result ) ) {
203
			return $result;
204
		}
205
206
		return true;
207
	}
208
209
	/**
210
	 * Perform a bulk upgrade.
211
	 *
212
	 * @since 1.0.0
213
	 *
214
	 * @param string[] $modules Array of basename paths to the modules.
215
	 * @param array    $args {
216
	 *        @type bool $clear_update_cache Whether the to clear the update cache.
217
	 *                                       Default is true.
218
	 * }
219
	 *
220
	 * @return array The result of each update, indexed by module.
221
	 */
222
	public function bulk_upgrade( $modules, $args = array() ) {
223
224
		$this->bulk = true;
225
226
		$args = $this->_before_upgrade( $args );
227
228
		$this->skin->header();
229
230
		// Connect to the Filesystem first.
231
		if ( ! $this->fs_connect( array( WP_CONTENT_DIR, wordpoints_modules_dir() ) ) ) {
232
233
			$this->skin->footer();
234
			return false;
235
		}
236
237
		$this->skin->bulk_header();
238
239
		$this->maybe_start_maintenance_mode( $modules );
240
241
		$results = array();
242
243
		$this->update_count = count( $modules );
244
		$this->update_current = 0;
245
246
		foreach ( $modules as $module ) {
247
248
			$this->update_current++;
249
250
			$results[ $module ] = $this->_upgrade( $module );
251
252
			// Prevent credentials auth screen from displaying multiple times.
253
			if ( false === $results[ $module ] && ! $this->bailed_early ) {
254
				break;
255
			}
256
		}
257
258
		$this->maintenance_mode( false );
259
260
		$this->skin->bulk_footer();
261
		$this->skin->footer();
262
263
		$this->_after_upgrade( $modules, $args );
264
265
		return $results;
266
267
	} // function bulk_upgrade()
268
269
	/**
270
	 * Set up before running an upgrade.
271
	 *
272
	 * @since 1.0.0
273
	 *
274
	 * @param array $args The arguments passed to the upgrader.
275
	 *
276
	 * @return array The parsed upgrader arguments.
277
	 */
278
	protected function _before_upgrade( $args ) {
279
280
		$args = wp_parse_args( $args, array( 'clear_update_cache' => true ) );
281
282
		$this->init();
283
		$this->upgrade_strings();
284
285
		add_filter( 'upgrader_clear_destination', array( $this, 'delete_old_module' ), 10, 4 );
286
		add_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
287
		add_filter( 'upgrader_source_selection', array( $this, 'correct_module_dir_name' ), 10, 3 );
288
289
		if ( ! $this->bulk ) {
290
			add_filter( 'upgrader_pre_install', array( $this, 'deactivate_module_before_upgrade' ), 10, 2 );
291
		}
292
293
		return $args;
294
	}
295
296
	/**
297
	 * Upgrade a module.
298
	 *
299
	 * This is the real meat of upgrade functions.
300
	 *
301
	 * @since 1.0.0
302
	 *
303
	 * @param string $module_file Basename path to the module file.
304
	 *
305
	 * @return mixed Returns true or an array on success, false or a WP_Error on failure.
306
	 */
307
	protected function _upgrade( $module_file ) {
308
309
		$this->bailed_early = false;
310
311
		$modules = wordpoints_get_modules();
312
313
		if ( ! isset( $modules[ $module_file ] ) ) {
314
			$this->_bail_early( 'not_installed' );
315
			return false;
316
		}
317
318
		$module_data = $modules[ $module_file ];
319
320
		if ( $this->bulk ) {
321
322
			$this->skin->module = $module_file;
323
			$this->skin->module_info = wordpoints_get_module_data(
324
				wordpoints_modules_dir() . $module_file
325
			);
326
			$this->skin->module_active = is_wordpoints_module_active( $module_file );
327
		}
328
329
		$current = get_site_transient( 'wordpoints_module_updates' );
330
331
		if ( ! isset( $current['response'][ $module_file ] ) ) {
332
			$this->_bail_early( 'up_to_date', 'feedback' );
333
			return true;
334
		}
335
336
		$channel = wordpoints_get_channel_for_module( $module_data );
337
		$channel = WordPoints_Module_Channels::get( $channel );
338
339
		if ( ! $channel ) {
340
			$this->_bail_early( 'no_channel' );
341
			return false;
342
		}
343
344
		$api = $channel->get_api();
345
346
		if ( false === $api ) {
347
			$this->_bail_early( 'api_not_found' );
348
			return false;
349
		}
350
351
		return $this->run(
352
			array(
353
				'package'           => $api->get_package_url( $channel, $module_data ),
354
				'destination'       => wordpoints_modules_dir(),
355
				'clear_destination' => true,
356
				'clear_working'     => true,
357
				'is_multi'          => $this->bulk,
358
				'hook_extra'        => array(
359
					'wordpoints_module' => $module_file,
360
				),
361
			)
362
		);
363
364
	} // function _upgrade()
365
366
	/**
367
	 * Clean up after an upgrade.
368
	 *
369
	 * @since 1.0.0
370
	 *
371
	 * @param string|string[] $modules The module(s) being upgraded.
372
	 * @param array           $args    The arguments passed to the upgrader.
373
	 */
374
	protected function _after_upgrade( $modules, $args ) {
375
376
		remove_filter( 'upgrader_source_selection', array( $this, 'check_package' ) );
377
		remove_filter( 'upgrader_source_selection', array( $this, 'correct_module_dir_name' ) );
378
		remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_module' ) );
379
380
		if ( ! $this->bulk ) {
381
382
			remove_filter( 'upgrader_pre_install', array( $this, 'deactivate_module_before_upgrade' ) );
383
384
			if ( ! $this->skin->result || is_wp_error( $this->skin->result ) ) {
385
				return;
386
			}
387
		}
388
389
		// Force refresh of module update cache.
390
		wordpointsorg_clean_modules_cache( $args['clear_update_cache'] );
391
392
		$details = array(
393
			'action' => 'update',
394
			'type'   => 'wordpoints_module',
395
			'bulk'   => $this->bulk,
396
		);
397
398
		/**
399
		 * This action is documented in /wp-admin/includes/class-wp-upgrader.php.
400
		 */
401
		do_action( 'upgrader_process_complete', $this, $details, $modules );
402
	}
403
404
	/**
405
	 * Conditionally start maintenance mode, only if necessary.
406
	 *
407
	 * Used when performing bulk updates.
408
	 *
409
	 * Only start maintenance mode if:
410
	 * - running Multisite and there are one or more modules specified, OR
411
	 * - a module with an update available is currently active.
412
	 *
413
	 * @since 1.0.0
414
	 *
415
	 * @param string[] $modules The modules being upgraded in bulk.
416
	 */
417
	public function maybe_start_maintenance_mode( $modules ) {
418
419
		if ( is_multisite() && ! empty( $modules ) ) {
420
421
			$this->maintenance_mode( true );
422
423
		} else {
424
425
			$current = get_site_transient( 'wordpoints_module_updates' );
426
427
			foreach ( $modules as $module ) {
428
429
				if (
430
					is_wordpoints_module_active( $module )
431
					&& isset( $current['response'][ $module ] )
432
				) {
433
					$this->maintenance_mode( true );
434
					break;
435
				}
436
			}
437
		}
438
	}
439
440
	/**
441
	 * Check if the source package actually contains a module.
442
	 *
443
	 * @since 1.0.0
444
	 *
445
	 * @WordPress\filter upgrader_source_selection Added by self::install().
446
	 *
447
	 * @uses $wp_filesystem
448
	 *
449
	 * @param string|WP_Error $source The path to the source package.
450
	 *
451
	 * @return string|WP_Error The path to the source package, or a WP_Error.
452
	 */
453
	public function check_package( $source ) {
454
455
		global $wp_filesystem;
456
457
		if ( is_wp_error( $source ) ) {
458
			return $source;
459
		}
460
461
		$working_directory = str_replace(
462
			$wp_filesystem->wp_content_dir()
463
			, trailingslashit( WP_CONTENT_DIR )
464
			, $source
465
		);
466
467
		if ( ! is_dir( $working_directory ) ) {
468
			return $source;
469
		}
470
471
		$modules_found = false;
472
473
		foreach ( glob( $working_directory . '*.php' ) as $file ) {
474
475
			$module_data = wordpoints_get_module_data( $file, false, false );
476
477
			if ( ! empty( $module_data['name'] ) ) {
478
				$modules_found = true;
479
				break;
480
			}
481
		}
482
483
		if ( ! $modules_found ) {
484
485
			return new WP_Error(
486
				'incompatible_archive_no_modules'
487
				, $this->strings['incompatible_archive']
488
				, __( 'No valid modules were found.', 'wordpointsorg' )
489
			);
490
		}
491
492
		return $source;
493
	}
494
495
	/**
496
	 * Get the file which contains the module info.
497
	 *
498
	 * Not used within the class, but is called by the installer skin.
499
	 *
500
	 * @since 1.0.0
501
	 *
502
	 * @return string|false The module path or false on failure.
503
	 */
504
	public function module_info() {
505
506
		if ( ! is_array( $this->result ) || empty( $this->result['destination_name'] ) ) {
507
			return false;
508
		}
509
510
		$module = wordpoints_get_modules( '/' . $this->result['destination_name'] );
511
512
		if ( empty( $module ) ) {
513
			return false;
514
		}
515
516
		return $this->result['destination_name'] . '/' . key( $module );
517
	}
518
519
	/**
520
	 * Make sure a module is inactive before it is upgraded.
521
	 *
522
	 * @since 1.0.0
523
	 *
524
	 * @WordPress\filter upgrader_pre_install Added by self::upgrade().
525
	 *
526
	 * @param bool|WP_Error $return True if we should do the upgrade, a WP_Error otherwise.
527
	 * @param array         $data   Data about the upgrade: what module is being upgraded.
528
	 *
529
	 * @return bool|WP_Error A WP_Error on failure, otherwise nothing.
530
	 */
531
	public function deactivate_module_before_upgrade( $return, $data ) {
532
533
		if ( is_wp_error( $return ) ) {
534
			return $return;
535
		}
536
537
		if ( empty( $data['wordpoints_module'] ) ) {
538
			return new WP_Error( 'bad_request', $this->strings['bad_request'] );
539
		}
540
541
		if ( is_wordpoints_module_active( $data['wordpoints_module'] ) ) {
542
543
			// Deactivate the module silently (the actions won't be fired).
544
			wordpoints_deactivate_modules( array( $data['wordpoints_module'] ), true );
545
		}
546
547
		return $return;
548
	}
549
550
	/**
551
	 * Ensures that a module folder will have the correct name.
552
	 *
553
	 * @since 1.0.0
554
	 *
555
	 * @WordPress\filter upgrader_source_selection Added by self::upgrade().
556
	 *
557
	 * @param string      $source        The path to the module source.
558
	 * @param array       $remote_source The remote source of the module.
559
	 * @param WP_Upgrader $upgrader      The upgrader instance.
560
	 *
561
	 * @return string The module folder.
562
	 */
563
	public function correct_module_dir_name( $source, $remote_source, $upgrader ) {
564
565
		global $wp_filesystem;
566
567
		if ( is_wp_error( $source ) ) {
568
			return $source;
569
		}
570
571
		if ( ! isset( $upgrader->skin->module ) ) {
572
			return $source;
573
		}
574
575
		$source_name = basename( $source );
576
		$module_name = dirname( $upgrader->skin->module );
577
578
		if ( '.' === $module_name || $source_name === $module_name ) {
579
			return $source;
580
		}
581
582
		$correct_source = dirname( $source ) . '/' . $module_name;
583
584
		$moved = $wp_filesystem->move( $source, $correct_source );
585
586
		if ( ! $moved ) {
587
			return new WP_Error( 'wordpointsorg_incorrect_source_name', $this->strings['incorrect_source_name'] );
588
		}
589
590
		return $correct_source;
591
	}
592
593
	/**
594
	 * Delete the old module before installing the new one.
595
	 *
596
	 * @since 1.0.0
597
	 *
598
	 * @WordPress\filter upgrader_clear_destination Added by self::upgrade() and
599
	 *                                              self::bulk_upgrade().
600
	 *
601
	 * @param true|WP_Error $removed            Whether the destination folder has been removed.
602
	 * @param string        $local_destination  The local path to the destination folder.
603
	 * @param string        $remote_destination The remote path to the destination folder.
604
	 * @param array         $data               Data for the upgrade: what module is being upgraded.
605
	 *
606
	 * @return true|WP_Error True on success, a WP_Error on failure.
607
	 */
608
	public function delete_old_module( $removed, $local_destination, $remote_destination, $data ) {
609
610
		global $wp_filesystem;
611
612
		if ( is_wp_error( $removed ) ) {
613
			return $removed;
614
		}
615
616
		if ( empty( $data['wordpoints_module'] ) ) {
617
			return new WP_Error( 'bad_request', $this->strings['bad_request'] );
618
		}
619
620
		$modules_dir = $wp_filesystem->find_folder( wordpoints_modules_dir() );
621
		$this_module_dir = trailingslashit( dirname( $modules_dir . $data['wordpoints_module'] ) );
622
623
		// Make sure it hasn't already been removed somehow.
624
		if ( ! $wp_filesystem->exists( $this_module_dir ) ) {
625
			return $removed;
626
		}
627
628
		/*
629
		 * If the module is in its own directory, recursively delete the directory.
630
		 * Do a base check on if the module includes the directory separator AND that
631
		 * it's not the root modules folder. If not, just delete the single file.
632
		 */
633
		if ( strpos( $data['wordpoints_module'], '/' ) && $this_module_dir !== $modules_dir ) {
634
			$deleted = $wp_filesystem->delete( $this_module_dir, true );
635
		} else {
636
			$deleted = $wp_filesystem->delete( $modules_dir . $data['wordpoints_module'] );
637
		}
638
639
		if ( ! $deleted ) {
640
			return new WP_Error( 'remove_old_failed', $this->strings['remove_old_failed'] );
641
		}
642
643
		return true;
644
	}
645
646
	//
647
	// Private Functions.
648
	//
649
650
	/**
651
	 * Bail early before finishing a a process normally.
652
	 *
653
	 * @since 1.0.0
654
	 *
655
	 * @param string $message Slug for the message to show the user.
656
	 * @param string $type    The type of message, 'error' (default), or 'feedback'.
657
	 */
658
	private function _bail_early( $message, $type = 'error' ) {
659
660
		$this->bailed_early = true;
661
662
		$this->skin->before();
663
		$this->skin->set_result( false );
664
665
		if ( 'feedback' === $type ) {
666
			$this->skin->feedback( $message );
667
		} else {
668
			$this->skin->error( $message );
669
		}
670
671
		$this->skin->after();
672
	}
673
674
} // class WordPoints_Module_Upgrader
675
676
// EOF
677