Test Failed
Push — master ( 9f58db...1a3aaa )
by Devin
05:56
created

EDD_SL_Plugin_Updater::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 3
dl 0
loc 29
rs 9.456
c 0
b 0
f 0
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.17
13
 */
14
class EDD_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 $cache_key   = '';
23
24
	private $health_check_timeout = 5;
0 ignored issues
show
Unused Code introduced by
The property $health_check_timeout is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
25
26
	/**
27
	 * Class constructor.
28
	 *
29
	 * @uses plugin_basename()
30
	 * @uses hook()
31
	 *
32
	 * @param string $_api_url     The URL pointing to the custom API endpoint.
33
	 * @param string $_plugin_file Path to the plugin file.
34
	 * @param array  $_api_data    Optional data to send with API calls.
35
	 */
36
	public function __construct( $_api_url, $_plugin_file, $_api_data = null ) {
37
38
		global $edd_plugin_data, $edd_plugin_url_available;
39
40
		$this->api_url = trailingslashit( $_api_url );
41
42
		$this->api_data    = $_api_data;
0 ignored issues
show
Documentation Bug introduced by
It seems like $_api_data can be null. However, the property $api_data is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
43
		$this->name        = plugin_basename( $_plugin_file );
44
		$this->slug        = basename( $_plugin_file, '.php' );
45
		$this->version     = $_api_data['version'];
46
		$this->wp_override = isset( $_api_data['wp_override'] ) ? (bool) $_api_data['wp_override'] : false;
47
		$this->beta        = ! empty( $this->api_data['beta'] ) ? true : false;
48
		$this->cache_key   = 'edd_sl_' . md5( serialize( $this->slug . $this->api_data['license'] . $this->beta ) );
49
50
		$edd_plugin_data[ $this->slug ] = $this->api_data;
51
52
		/**
53
		 * Fires after the $edd_plugin_data is setup.
54
		 *
55
		 * @since x.x.x
56
		 *
57
		 * @param array $edd_plugin_data Array of EDD SL plugin data.
58
		 */
59
		do_action( 'post_edd_sl_plugin_updater_setup', $edd_plugin_data );
60
61
		// Set up hooks.
62
		$this->init();
63
64
	}
65
66
	/**
67
	 * Set up WordPress filters to hook into WP's update process.
68
	 *
69
	 * @uses add_filter()
70
	 *
71
	 * @return void
72
	 */
73
	public function init() {
74
75
		add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
76
		add_filter( 'plugins_api', array( $this, 'plugins_api_filter' ), 10, 3 );
77
		remove_action( 'after_plugin_row_' . $this->name, 'wp_plugin_update_row', 10 );
78
		add_action( 'after_plugin_row_' . $this->name, array( $this, 'show_update_notification' ), 10, 2 );
79
		add_action( 'admin_init', array( $this, 'show_changelog' ) );
80
81
	}
82
83
	/**
84
	 * Check for Updates at the defined API endpoint and modify the update array.
85
	 *
86
	 * This function dives into the update API just when WordPress creates its update array,
87
	 * then adds a custom API call and injects the custom plugin data retrieved from the API.
88
	 * It is reassembled from parts of the native WordPress plugin update code.
89
	 * See wp-includes/update.php line 121 for the original wp_update_plugins() function.
90
	 *
91
	 * @uses api_request()
92
	 *
93
	 * @param array $_transient_data Update array build by WordPress.
94
	 * @return array Modified update array with custom plugin data.
95
	 */
96
	public function check_update( $_transient_data ) {
97
98
		global $pagenow;
99
100
		if ( ! is_object( $_transient_data ) ) {
101
			$_transient_data = new stdClass();
102
		}
103
104
		if ( 'plugins.php' == $pagenow && is_multisite() ) {
105
			return $_transient_data;
106
		}
107
108
		if ( ! empty( $_transient_data->response ) && ! empty( $_transient_data->response[ $this->name ] ) && false === $this->wp_override ) {
109
			return $_transient_data;
110
		}
111
112
		$version_info = $this->get_cached_version_info();
113
114 View Code Duplication
		if ( false === $version_info ) {
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...
115
			$version_info = $this->api_request(
116
				'plugin_latest_version', array(
117
					'slug' => $this->slug,
118
					'beta' => $this->beta,
119
				)
120
			);
121
122
			$this->set_version_info_cache( $version_info );
0 ignored issues
show
Documentation introduced by
$version_info is of type false|object, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
123
124
		}
125
126
		if ( false !== $version_info && is_object( $version_info ) && isset( $version_info->new_version ) ) {
127
128
			if ( version_compare( $this->version, $version_info->new_version, '<' ) ) {
129
130
				$_transient_data->response[ $this->name ] = $version_info;
131
132
			}
133
134
			$_transient_data->last_checked           = time();
135
			$_transient_data->checked[ $this->name ] = $this->version;
136
137
		}
138
139
		return $_transient_data;
140
	}
141
142
	/**
143
	 * show update nofication row -- needed for multisite subsites, because WP won't tell you otherwise!
144
	 *
145
	 * @param string $file
146
	 * @param array  $plugin
147
	 */
148
	public function show_update_notification( $file, $plugin ) {
149
150
		if ( is_network_admin() ) {
151
			return;
152
		}
153
154
		if ( ! current_user_can( 'update_plugins' ) ) {
155
			return;
156
		}
157
158
		if ( ! is_multisite() ) {
159
			return;
160
		}
161
162
		if ( $this->name != $file ) {
163
			return;
164
		}
165
166
		// Remove our filter on the site transient
167
		remove_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ), 10 );
168
169
		$update_cache = get_site_transient( 'update_plugins' );
170
171
		$update_cache = is_object( $update_cache ) ? $update_cache : new stdClass();
172
173
		if ( empty( $update_cache->response ) || empty( $update_cache->response[ $this->name ] ) ) {
174
175
			$version_info = $this->get_cached_version_info();
176
177 View Code Duplication
			if ( false === $version_info ) {
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...
178
				$version_info = $this->api_request(
179
					'plugin_latest_version', array(
180
						'slug' => $this->slug,
181
						'beta' => $this->beta,
182
					)
183
				);
184
185
				$this->set_version_info_cache( $version_info );
0 ignored issues
show
Documentation introduced by
$version_info is of type false|object, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
186
			}
187
188
			if ( ! is_object( $version_info ) ) {
189
				return;
190
			}
191
192
			if ( version_compare( $this->version, $version_info->new_version, '<' ) ) {
193
194
				$update_cache->response[ $this->name ] = $version_info;
195
196
			}
197
198
			$update_cache->last_checked           = time();
199
			$update_cache->checked[ $this->name ] = $this->version;
200
201
			set_site_transient( 'update_plugins', $update_cache );
202
203
		} else {
204
205
			$version_info = $update_cache->response[ $this->name ];
206
207
		}
208
209
		// Restore our filter
210
		add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
211
212
		if ( ! empty( $update_cache->response[ $this->name ] ) && version_compare( $this->version, $version_info->new_version, '<' ) ) {
213
214
			// build a plugin list row, with update notification
215
			$wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
0 ignored issues
show
Unused Code introduced by
$wp_list_table is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
216
			// <tr class="plugin-update-tr"><td colspan="' . $wp_list_table->get_column_count() . '" class="plugin-update colspanchange">
217
			echo '<tr class="plugin-update-tr" id="' . $this->slug . '-update" data-slug="' . $this->slug . '" data-plugin="' . $this->slug . '/' . $file . '">';
0 ignored issues
show
introduced by
Expected next thing to be a escaping function, not '$this'
Loading history...
introduced by
Expected next thing to be a escaping function, not '$file'
Loading history...
218
			echo '<td colspan="3" class="plugin-update colspanchange">';
219
			echo '<div class="update-message notice inline notice-warning notice-alt">';
220
221
			$changelog_link = self_admin_url( 'index.php?edd_sl_action=view_plugin_changelog&plugin=' . $this->name . '&slug=' . $this->slug . '&TB_iframe=true&width=772&height=911' );
222
223
			if ( empty( $version_info->download_link ) ) {
224
				printf(
225
					__( 'There is a new version of %1$s available. %2$sView version %3$s details%4$s.', 'give' ),
226
					esc_html( $version_info->name ),
227
					'<a target="_blank" class="thickbox" href="' . esc_url( $changelog_link ) . '">',
228
					esc_html( $version_info->new_version ),
229
					'</a>'
230
				);
231
			} else {
232
				printf(
233
					__( 'There is a new version of %1$s available. %2$sView version %3$s details%4$s or %5$supdate now%6$s.', 'give' ),
234
					esc_html( $version_info->name ),
235
					'<a target="_blank" class="thickbox" href="' . esc_url( $changelog_link ) . '">',
236
					esc_html( $version_info->new_version ),
237
					'</a>',
238
					'<a href="' . esc_url( wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $this->name, 'upgrade-plugin_' . $this->name ) ) . '">',
239
					'</a>'
240
				);
241
			}
242
243
			do_action( "in_plugin_update_message-{$file}", $plugin, $version_info );
244
245
			echo '</div></td></tr>';
246
		}
247
	}
248
249
	/**
250
	 * Updates information on the "View version x.x details" page with custom data.
251
	 *
252
	 * @uses api_request()
253
	 *
254
	 * @param mixed  $_data
255
	 * @param string $_action
256
	 * @param object $_args
257
	 * @return object $_data
258
	 */
259
	public function plugins_api_filter( $_data, $_action = '', $_args = null ) {
260
261
		if ( $_action != 'plugin_information' ) {
0 ignored issues
show
introduced by
Found "!= '". Use Yoda Condition checks, you must
Loading history...
262
263
			return $_data;
264
265
		}
266
267
		if ( ! isset( $_args->slug ) || ( $_args->slug != $this->slug ) ) {
268
269
			return $_data;
270
271
		}
272
273
		$to_send = array(
274
			'slug'   => $this->slug,
275
			'is_ssl' => is_ssl(),
276
			'fields' => array(
277
				'banners' => array(),
278
				'reviews' => false,
279
			),
280
		);
281
282
		$cache_key = 'edd_api_request_' . md5( serialize( $this->slug . $this->api_data['license'] . $this->beta ) );
283
284
		// Get the transient where we store the api request for this plugin for 24 hours
285
		$edd_api_request_transient = $this->get_cached_version_info( $cache_key );
286
287
		// 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.
288
		if ( empty( $edd_api_request_transient ) ) {
289
290
			$api_response = $this->api_request( 'plugin_information', $to_send );
291
292
			// Expires in 3 hours
293
			$this->set_version_info_cache( $api_response, $cache_key );
0 ignored issues
show
Documentation introduced by
$api_response is of type false|object, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
294
295
			if ( false !== $api_response ) {
296
				$_data = $api_response;
297
			}
298
		} else {
299
			$_data = $edd_api_request_transient;
300
		}
301
302
		// Convert sections into an associative array, since we're getting an object, but Core expects an array.
303 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...
304
			$new_sections = array();
305
			foreach ( $_data->sections as $key => $value ) {
306
				$new_sections[ $key ] = $value;
307
			}
308
309
			$_data->sections = $new_sections;
310
		}
311
312
		// Convert banners into an associative array, since we're getting an object, but Core expects an array.
313 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...
314
			$new_banners = array();
315
			foreach ( $_data->banners as $key => $value ) {
316
				$new_banners[ $key ] = $value;
317
			}
318
319
			$_data->banners = $new_banners;
320
		}
321
322
		return $_data;
323
	}
324
325
	/**
326
	 * Disable SSL verification in order to prevent download update failures
327
	 *
328
	 * @param array  $args
329
	 * @param string $url
330
	 * @return object $array
331
	 */
332
	public function http_request_args( $args, $url ) {
333
334
		$verify_ssl = $this->verify_ssl();
335
		if ( strpos( $url, 'https://' ) !== false && strpos( $url, 'edd_action=package_download' ) ) {
0 ignored issues
show
introduced by
Found "!== false". Use Yoda Condition checks, you must
Loading history...
336
			$args['sslverify'] = $verify_ssl;
337
		}
338
		return $args;
339
340
	}
341
342
	/**
343
	 * Calls the API and, if successfull, returns the object delivered by the API.
344
	 *
345
	 * @uses get_bloginfo()
346
	 * @uses wp_remote_post()
347
	 * @uses is_wp_error()
348
	 *
349
	 * @param string $_action The requested action.
350
	 * @param array  $_data   Parameters for the API action.
351
	 * @return false|object
352
	 */
353
	private function api_request( $_action, $_data ) {
0 ignored issues
show
Unused Code introduced by
The parameter $_action 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...
354
355
		global $wp_version;
356
357
		$data = array_merge( $this->api_data, $_data );
358
359
		if ( $data['slug'] != $this->slug ) {
360
			return;
361
		}
362
363
		if ( $this->api_url == trailingslashit( home_url() ) ) {
364
			return false; // Don't allow a plugin to ping itself
365
		}
366
367
		$api_params = array(
368
			'edd_action' => 'get_version',
369
			'license'    => ! empty( $data['license'] ) ? $data['license'] : '',
370
			'item_name'  => isset( $data['item_name'] ) ? $data['item_name'] : false,
371
			'item_id'    => isset( $data['item_id'] ) ? $data['item_id'] : false,
372
			'version'    => isset( $data['version'] ) ? $data['version'] : false,
373
			'slug'       => $data['slug'],
374
			'author'     => $data['author'],
375
			'url'        => home_url(),
376
			'beta'       => ! empty( $data['beta'] ),
377
		);
378
379
		$verify_ssl = $this->verify_ssl();
380
		$request    = wp_remote_post(
381
			$this->api_url, array(
382
				'timeout'   => 15,
383
				'sslverify' => $verify_ssl,
384
				'body'      => $api_params,
385
			)
386
		);
387
388
		if ( ! is_wp_error( $request ) ) {
389
			$request = json_decode( wp_remote_retrieve_body( $request ) );
390
		}
391
392
		if ( $request && isset( $request->sections ) ) {
393
			$request->sections = maybe_unserialize( $request->sections );
394
		} else {
395
			$request = false;
396
		}
397
398
		if ( $request && isset( $request->banners ) ) {
399
			$request->banners = maybe_unserialize( $request->banners );
400
		}
401
402
		if ( ! empty( $request->sections ) ) {
403
			foreach ( $request->sections as $key => $section ) {
404
				$request->$key = (array) $section;
405
			}
406
		}
407
408
		return $request;
409
	}
410
411
	public function show_changelog() {
412
413
		global $edd_plugin_data;
414
415
		if ( empty( $_REQUEST['edd_sl_action'] ) || 'view_plugin_changelog' != $_REQUEST['edd_sl_action'] ) {
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_REQUEST
Loading history...
416
			return;
417
		}
418
419
		if ( empty( $_REQUEST['plugin'] ) ) {
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
420
			return;
421
		}
422
423
		if ( empty( $_REQUEST['slug'] ) ) {
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
424
			return;
425
		}
426
427
		if ( ! current_user_can( 'update_plugins' ) ) {
428
			wp_die( __( 'You do not have permission to install plugin updates', 'give' ), __( 'Error', 'give' ), array( 'response' => 403 ) );
429
		}
430
431
		$data         = $edd_plugin_data[ $_REQUEST['slug'] ];
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_REQUEST
Loading history...
432
		$beta         = ! empty( $data['beta'] ) ? true : false;
433
		$cache_key    = md5( 'edd_plugin_' . sanitize_key( $_REQUEST['plugin'] ) . '_' . $beta . '_version_info' );
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
434
		$version_info = $this->get_cached_version_info( $cache_key );
435
436
		if ( false === $version_info ) {
437
438
			$api_params = array(
439
				'edd_action' => 'get_version',
440
				'item_name'  => isset( $data['item_name'] ) ? $data['item_name'] : false,
441
				'item_id'    => isset( $data['item_id'] ) ? $data['item_id'] : false,
442
				'slug'       => $_REQUEST['slug'],
0 ignored issues
show
introduced by
Detected access of super global var $_REQUEST, probably need manual inspection.
Loading history...
introduced by
Detected usage of a non-sanitized input variable: $_REQUEST
Loading history...
443
				'author'     => $data['author'],
444
				'url'        => home_url(),
445
				'beta'       => ! empty( $data['beta'] ),
446
			);
447
448
			$verify_ssl = $this->verify_ssl();
449
			$request    = wp_remote_post(
450
				$this->api_url, array(
451
					'timeout'   => 15,
452
					'sslverify' => $verify_ssl,
453
					'body'      => $api_params,
454
				)
455
			);
456
457
			if ( ! is_wp_error( $request ) ) {
458
				$version_info = json_decode( wp_remote_retrieve_body( $request ) );
459
			}
460
461
			if ( ! empty( $version_info ) && isset( $version_info->sections ) ) {
462
				$version_info->sections = maybe_unserialize( $version_info->sections );
463
			} else {
464
				$version_info = false;
465
			}
466
467
			if ( ! empty( $version_info ) ) {
468
				foreach ( $version_info->sections as $key => $section ) {
469
					$version_info->$key = (array) $section;
470
				}
471
			}
472
473
			$this->set_version_info_cache( $version_info, $cache_key );
474
475
		}
476
477
		if ( ! empty( $version_info ) && isset( $version_info->sections['changelog'] ) ) {
478
			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...
479
		}
480
481
		exit;
482
	}
483
484
	public function get_cached_version_info( $cache_key = '' ) {
485
486
		if ( empty( $cache_key ) ) {
487
			$cache_key = $this->cache_key;
488
		}
489
490
		$cache = get_option( $cache_key );
491
492
		if ( empty( $cache['timeout'] ) || time() > $cache['timeout'] ) {
493
			return false; // Cache is expired
494
		}
495
496
		return json_decode( $cache['value'] );
497
498
	}
499
500
	public function set_version_info_cache( $value = '', $cache_key = '' ) {
501
502
		if ( empty( $cache_key ) ) {
503
			$cache_key = $this->cache_key;
504
		}
505
506
		$data = array(
507
			'timeout' => strtotime( '+3 hours', time() ),
508
			'value'   => json_encode( $value ),
509
		);
510
511
		update_option( $cache_key, $data, 'no' );
512
513
	}
514
515
	/**
516
	 * Returns if the SSL of the store should be verified.
517
	 *
518
	 * @since  1.6.13
519
	 * @return bool
520
	 */
521
	private function verify_ssl() {
522
		return (bool) apply_filters( 'edd_sl_api_request_verify_ssl', true, $this );
523
	}
524
525
}
526