Completed
Push — master ( b9eecd...572f19 )
by Stephanie
08:04
created

FrmEDD_SL_Plugin_Updater   C

Complexity

Total Complexity 67

Size/Duplication

Total Lines 382
Duplicated Lines 4.19 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 16
loc 382
rs 5.7097
c 0
b 0
f 0
wmc 67
lcom 1
cbo 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 26 3
A init() 0 5 1
C check_update() 0 43 11
C plugins_api_filter() 16 66 12
A http_request_args() 0 9 3
C api_request() 0 41 8
B prepare_response() 0 17 7
D show_changelog() 0 66 17
A get_cached_version_info() 0 10 3
A set_version_info_cache() 0 9 1
A verify_ssl() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like FrmEDD_SL_Plugin_Updater 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 FrmEDD_SL_Plugin_Updater, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
// Exit if accessed directly
4
if ( ! defined( 'ABSPATH' ) ) {
5
	exit;
6
}
7
8
/**
9
 * Allows plugins to use their own update API.
10
 *
11
 * @author Easy Digital Downloads
12
 * @version 1.6.15
13
 */
14
class FrmEDD_SL_Plugin_Updater {
15
16
	private $api_url     = '';
17
	private $api_data    = array();
18
	private $name        = '';
19
	private $slug        = '';
20
	private $version     = '';
21
	private $wp_override = false;
22
	private $beta        = false;
23
	private $cache_key   = '';
24
25
	/**
26
	 * Class constructor.
27
	 *
28
	 * @uses plugin_basename()
29
	 * @uses hook()
30
	 *
31
	 * @param string  $_api_url     The URL pointing to the custom API endpoint.
32
	 * @param string  $_plugin_file Path to the plugin file.
33
	 * @param array   $_api_data    Optional data to send with API calls.
34
	 */
35
	public function __construct( $_api_url, $_plugin_file, $_api_data = array() ) {
36
		global $frm_edd_plugin_data;
37
38
		$this->api_url     = trailingslashit( $_api_url );
39
		$this->api_data    = $_api_data;
40
		$this->name        = plugin_basename( $_plugin_file );
41
		$this->slug        = basename( $_plugin_file, '.php' );
42
		$this->version     = $_api_data['version'];
43
		$this->wp_override = isset( $_api_data['wp_override'] ) ? (bool) $_api_data['wp_override'] : false;
44
		$this->beta        = ! empty( $this->api_data['beta'] ) ? true : false;
45
		$this->cache_key   = md5( serialize( $this->slug . $this->version . $this->api_data['license'] . $this->beta ) );
46
47
		$frm_edd_plugin_data[ $this->slug ] = $this->api_data;
48
49
		/**
50
		 * Fires after the $frm_edd_plugin_data is setup.
51
		 *
52
		 * @since x.x.x
53
		 *
54
		 * @param array $frm_edd_plugin_data Array of EDD SL plugin data.
55
		 */
56
		do_action( 'post_edd_sl_plugin_updater_setup', $frm_edd_plugin_data );
57
58
		// Set up hooks.
59
		$this->init();
60
	}
61
62
	/**
63
	 * Set up WordPress filters to hook into WP's update process.
64
	 *
65
	 * @uses add_filter()
66
	 *
67
	 * @return void
68
	 */
69
	public function init() {
70
		add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
71
		add_filter( 'plugins_api', array( $this, 'plugins_api_filter' ), 10, 3 );
72
		add_action( 'admin_init', array( $this, 'show_changelog' ) );
73
	}
74
75
	/**
76
	 * Check for Updates at the defined API endpoint and modify the update array.
77
	 *
78
	 * This function dives into the update API just when WordPress creates its update array,
79
	 * then adds a custom API call and injects the custom plugin data retrieved from the API.
80
	 * It is reassembled from parts of the native WordPress plugin update code.
81
	 * See wp-includes/update.php line 121 for the original wp_update_plugins() function.
82
	 *
83
	 * @uses api_request()
84
	 *
85
	 * @param array   $_transient_data Update array build by WordPress.
86
	 * @return array Modified update array with custom plugin data.
87
	 */
88
	public function check_update( $_transient_data ) {
89
90
		global $pagenow;
91
92
		if ( ! is_object( $_transient_data ) ) {
93
			$_transient_data = new stdClass;
94
		}
95
96
		if ( ! empty( $_transient_data->response ) && ! empty( $_transient_data->response[ $this->name ] ) && false === $this->wp_override ) {
97
			return $_transient_data;
98
		}
99
100
		$version_info = $this->get_cached_version_info( $this->cache_key );
101
102
		if ( false === $version_info ) {
103
			$version_info = $this->api_request( 'plugin_latest_version', array(
104
				'slug' => $this->slug,
105
				'beta' => $this->beta,
106
			) );
107
108
			$this->set_version_info_cache( $version_info, $this->cache_key );
109
110
		}
111
112
		if ( false !== $version_info && is_object( $version_info ) && isset( $version_info->new_version ) ) {
113
114
			if ( version_compare( $this->version, $version_info->new_version, '<' ) ) {
115
116
				if ( empty( $version_info->plugin ) ) {
117
					$version_info->plugin = $this->name;
118
				}
119
120
				$_transient_data->response[ $this->name ] = $version_info;
121
122
			}
123
124
			$_transient_data->last_checked           = time();
125
			$_transient_data->checked[ $this->name ] = $this->version;
126
127
		}
128
129
		return $_transient_data;
130
	}
131
132
	/**
133
	 * Updates information on the "View version x.x details" page with custom data.
134
	 *
135
	 * @uses api_request()
136
	 *
137
	 * @param mixed   $_data
138
	 * @param string  $_action
139
	 * @param object  $_args
140
	 * @return object $_data
141
	 */
142
	public function plugins_api_filter( $_data, $_action = '', $_args = null ) {
143
144
		if ( $_action != 'plugin_information' ) {
145
146
			return $_data;
147
148
		}
149
150
		if ( ! isset( $_args->slug ) || ( $_args->slug != $this->slug ) ) {
151
152
			return $_data;
153
154
		}
155
156
		$to_send = array(
157
			'slug'   => $this->slug,
158
			'is_ssl' => is_ssl(),
159
			'fields' => array(
160
				'banners' => array(),
161
				'reviews' => false,
162
			),
163
		);
164
165
		$cache_key = 'edd_api_request_' . $this->cache_key;
166
167
		// Get the transient where we store the api request for this plugin for 24 hours
168
		$edd_api_request_transient = $this->get_cached_version_info( $cache_key );
169
170
		//If we have no transient-saved value, run the API, set a fresh transient with the API value, and return that value too right now.
171
		if ( empty( $edd_api_request_transient ) ) {
172
173
			$api_response = $this->api_request( 'plugin_information', $to_send );
174
175
			// Expires in 3 hours
176
			$this->set_version_info_cache( $api_response, $cache_key );
177
178
			if ( false !== $api_response ) {
179
				$_data = $api_response;
180
			}
0 ignored issues
show
introduced by
Blank line found after control structure
Loading history...
181
182
		} else {
183
			$_data = $edd_api_request_transient;
184
		}
185
186
		// Convert sections into an associative array, since we're getting an object, but Core expects an array.
187 View Code Duplication
		if ( isset( $_data->sections ) && ! is_array( $_data->sections ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
188
			$new_sections = array();
189
			foreach ( $_data->sections as $key => $value ) {
190
				$new_sections[ $key ] = $value;
191
			}
192
193
			$_data->sections = $new_sections;
194
		}
195
196
		// Convert banners into an associative array, since we're getting an object, but Core expects an array.
197 View Code Duplication
		if ( isset( $_data->banners ) && ! is_array( $_data->banners ) ) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
198
			$new_banners = array();
199
			foreach ( $_data->banners as $key => $value ) {
200
				$new_banners[ $key ] = $value;
201
			}
202
203
			$_data->banners = $new_banners;
204
		}
205
206
		return $_data;
207
	}
208
209
	/**
210
	 * Disable SSL verification in order to prevent download update failures
211
	 *
212
	 * @param array   $args
213
	 * @param string  $url
214
	 * @return object $array
215
	 */
216
	public function http_request_args( $args, $url ) {
217
218
		$verify_ssl = $this->verify_ssl();
219
		if ( strpos( $url, 'https://' ) !== false && strpos( $url, 'edd_action=package_download' ) ) {
220
			$args['sslverify'] = $verify_ssl;
221
		}
222
		return $args;
223
224
	}
225
226
	/**
227
	 * Calls the API and, if successfull, returns the object delivered by the API.
228
	 *
229
	 * @uses get_bloginfo()
230
	 * @uses wp_remote_post()
231
	 * @uses is_wp_error()
232
	 *
233
	 * @param string  $_action The requested action.
234
	 * @param array   $_data   Parameters for the API action.
235
	 * @return false|object
236
	 */
237
	private function api_request( $_action, $_data ) {
238
239
		global $wp_version;
240
241
		$data = array_merge( $this->api_data, $_data );
242
243
		if ( $data['slug'] != $this->slug ) {
244
			return;
245
		}
246
247
		if ( $this->api_url == trailingslashit( home_url() ) ) {
248
			return false; // Don't allow a plugin to ping itself
249
		}
250
251
		$api_params = array(
252
			'edd_action' => 'get_version',
253
			'license'    => ! empty( $data['license'] ) ? $data['license'] : '',
254
			'item_name'  => isset( $data['item_name'] ) ? $data['item_name'] : false,
255
			'item_id'    => isset( $data['item_id'] ) ? $data['item_id'] : false,
256
			'version'    => isset( $data['version'] ) ? $data['version'] : false,
257
			'slug'       => $data['slug'],
258
			'author'     => $data['author'],
259
			'url'        => home_url(),
260
			'beta'       => ! empty( $data['beta'] ),
261
		);
262
263
		$verify_ssl = $this->verify_ssl();
264
		$request    = wp_remote_post( $this->api_url, array(
265
			'timeout'   => 15,
266
			'sslverify' => $verify_ssl,
267
			'body'      => $api_params,
268
		) );
269
270
		if ( ! is_wp_error( $request ) ) {
271
			$request = json_decode( wp_remote_retrieve_body( $request ) );
272
		}
273
274
		$this->prepare_response( $request );
275
276
		return $request;
277
	}
278
279
	private function prepare_response( &$request ) {
280
		if ( $request && isset( $request->sections ) ) {
281
			$request->sections = maybe_unserialize( $request->sections );
282
		} else {
283
			$request = false;
284
		}
285
286
		if ( $request && isset( $request->banners ) ) {
287
			$request->banners = maybe_unserialize( $request->banners );
288
		}
289
290
		if ( ! empty( $request->sections ) ) {
291
			foreach ( $request->sections as $key => $section ) {
292
				$request->$key = (array) $section;
293
			}
294
		}
295
	}
296
297
	public function show_changelog() {
298
299
		global $frm_edd_plugin_data;
300
301
		if ( empty( $_REQUEST['edd_sl_action'] ) || 'view_plugin_changelog' !== $_REQUEST['edd_sl_action'] ) {
302
			return;
303
		}
304
305
		if ( empty( $_REQUEST['plugin'] ) || empty( $_REQUEST['slug'] ) ) {
306
			return;
307
		}
308
309
		if ( ! current_user_can( 'update_plugins' ) ) {
310
			wp_die( __( 'You do not have permission to install plugin updates', 'formidable' ), __( 'Error', 'formidable' ), array( 'response' => 403 ) );
311
		}
312
313
		$data         = $frm_edd_plugin_data[ $_REQUEST['slug'] ];
314
		$beta         = ! empty( $data['beta'] ) ? true : false;
315
		$cache_key    = md5( 'edd_plugin_' . sanitize_key( $_REQUEST['plugin'] ) . '_' . $beta . '_version_info' );
316
		$version_info = $this->get_cached_version_info( $cache_key );
317
318
		if ( false === $version_info ) {
319
320
			$api_params = array(
321
				'edd_action' => 'get_version',
322
				'item_name'  => isset( $data['item_name'] ) ? $data['item_name'] : false,
323
				'item_id'    => isset( $data['item_id'] ) ? $data['item_id'] : false,
324
				'slug'       => sanitize_text_field( $_REQUEST['slug'] ),
325
				'author'     => $data['author'],
326
				'url'        => home_url(),
327
				'beta'       => $beta,
328
			);
329
330
			$verify_ssl = $this->verify_ssl();
331
			$request    = wp_remote_post( $this->api_url, array(
332
				'timeout'   => 15,
333
				'sslverify' => $verify_ssl,
334
				'body'      => $api_params,
335
			) );
336
337
			if ( ! is_wp_error( $request ) ) {
338
				$version_info = json_decode( wp_remote_retrieve_body( $request ) );
339
			}
340
341
			if ( ! empty( $version_info ) && isset( $version_info->sections ) ) {
342
				$version_info->sections = maybe_unserialize( $version_info->sections );
343
			} else {
344
				$version_info = false;
345
			}
346
347
			if ( ! empty( $version_info ) ) {
348
				foreach ( $version_info->sections as $key => $section ) {
349
					$version_info->$key = (array) $section;
350
				}
351
			}
352
353
			$this->set_version_info_cache( $version_info, $cache_key );
354
355
		}
356
357
		if ( ! empty( $version_info ) && isset( $version_info->sections['changelog'] ) ) {
358
			echo '<div style="background:#fff;padding:10px;">' . $version_info->sections['changelog'] . '</div>';
0 ignored issues
show
introduced by
Expected next thing to be a escaping function, not '$version_info'
Loading history...
359
		}
360
361
		exit;
362
	}
363
364
	public function get_cached_version_info( $cache_key = '' ) {
365
		$cache = get_option( $cache_key );
366
367
		if ( empty( $cache['timeout'] ) || current_time( 'timestamp' ) > $cache['timeout'] ) {
368
			return false; // Cache is expired
369
		}
370
371
		return json_decode( $cache['value'] );
372
373
	}
374
375
	public function set_version_info_cache( $value = '', $cache_key = '' ) {
376
		$data = array(
377
			'timeout' => strtotime( '+24 hours', current_time( 'timestamp' ) ),
378
			'value'   => json_encode( $value ),
379
		);
380
381
		update_option( $cache_key, $data, 'no' );
382
383
	}
384
385
	/**
386
	 * Returns if the SSL of the store should be verified.
387
	 *
388
	 * @since  1.6.13
389
	 * @return bool
390
	 */
391
	private function verify_ssl() {
392
		return (bool) apply_filters( 'edd_sl_api_request_verify_ssl', true, $this );
393
	}
394
395
}
396