Completed
Push — master ( 9e3902...3c0417 )
by Devin
39:54 queued 19:56
created

EDD_SL_Plugin_Updater::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 19
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 12
nc 4
nop 3
dl 0
loc 19
rs 9.4285
c 0
b 0
f 0
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 12 and the first side effect is on line 4.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
// Exit if accessed directly
4
if ( ! defined( 'ABSPATH' ) ) exit;
5
6
/**
7
 * Allows plugins to use their own update API.
8
 *
9
 * @author Easy Digital Downloads
10
 * @version 1.6.11
11
 */
12
class EDD_SL_Plugin_Updater {
13
14
	private $api_url     = '';
15
	private $api_data    = array();
16
	private $name        = '';
17
	private $slug        = '';
18
	private $version     = '';
19
	private $wp_override = false;
20
	private $cache_key   = '';
21
22
	/**
23
	 * Class constructor.
24
	 *
25
	 * @uses plugin_basename()
26
	 * @uses hook()
27
	 *
28
	 * @param string  $_api_url     The URL pointing to the custom API endpoint.
29
	 * @param string  $_plugin_file Path to the plugin file.
30
	 * @param array   $_api_data    Optional data to send with API calls.
31
	 */
32
	public function __construct( $_api_url, $_plugin_file, $_api_data = null ) {
33
34
		global $edd_plugin_data;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
35
36
		$this->api_url     = trailingslashit( $_api_url );
37
		$this->api_data    = $_api_data;
38
		$this->name        = plugin_basename( $_plugin_file );
39
		$this->slug        = basename( $_plugin_file, '.php' );
40
		$this->version     = $_api_data['version'];
41
		$this->wp_override = isset( $_api_data['wp_override'] ) ? (bool) $_api_data['wp_override'] : false;
42
		$this->beta        = ! empty( $this->api_data['beta'] ) ? true : false;
0 ignored issues
show
Bug introduced by
The property beta does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
43
		$this->cache_key   = md5( serialize( $this->slug . $this->api_data['license'] . $this->beta ) );
44
45
		$edd_plugin_data[ $this->slug ] = $this->api_data;
46
47
		// Set up hooks.
48
		$this->init();
49
50
	}
51
52
	/**
53
	 * Set up WordPress filters to hook into WP's update process.
54
	 *
55
	 * @uses add_filter()
56
	 *
57
	 * @return void
58
	 */
59
	public function init() {
60
61
		add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
62
		add_filter( 'plugins_api', array( $this, 'plugins_api_filter' ), 10, 3 );
63
		remove_action( 'after_plugin_row_' . $this->name, 'wp_plugin_update_row', 10 );
64
		add_action( 'after_plugin_row_' . $this->name, array( $this, 'show_update_notification' ), 10, 2 );
65
		add_action( 'admin_init', array( $this, 'show_changelog' ) );
66
67
	}
68
69
	/**
70
	 * Check for Updates at the defined API endpoint and modify the update array.
71
	 *
72
	 * This function dives into the update API just when WordPress creates its update array,
73
	 * then adds a custom API call and injects the custom plugin data retrieved from the API.
74
	 * It is reassembled from parts of the native WordPress plugin update code.
75
	 * See wp-includes/update.php line 121 for the original wp_update_plugins() function.
76
	 *
77
	 * @uses api_request()
78
	 *
79
	 * @param array   $_transient_data Update array build by WordPress.
80
	 * @return array Modified update array with custom plugin data.
0 ignored issues
show
Documentation introduced by
Should the return type not be stdClass?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
81
	 */
82
	public function check_update( $_transient_data ) {
83
84
		global $pagenow;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
85
86
		if ( ! is_object( $_transient_data ) ) {
87
			$_transient_data = new stdClass;
88
		}
89
90
		if ( 'plugins.php' == $pagenow && is_multisite() ) {
91
			return $_transient_data;
92
		}
93
94
		if ( ! empty( $_transient_data->response ) && ! empty( $_transient_data->response[ $this->name ] ) && false === $this->wp_override ) {
95
			return $_transient_data;
96
		}
97
98
		$version_info = $this->get_cached_version_info();
99
100
		if ( false === $version_info ) {
101
			$version_info = $this->api_request( 'plugin_latest_version', array( 'slug' => $this->slug, 'beta' => $this->beta ) );
102
103
			$this->set_version_info_cache( $version_info );
104
105
		}
106
107
		if ( false !== $version_info && is_object( $version_info ) && isset( $version_info->new_version ) ) {
108
109
			if ( version_compare( $this->version, $version_info->new_version, '<' ) ) {
110
111
				$_transient_data->response[ $this->name ] = $version_info;
112
113
			}
114
115
			$_transient_data->last_checked           = current_time( 'timestamp' );
116
			$_transient_data->checked[ $this->name ] = $this->version;
117
118
		}
119
120
		return $_transient_data;
121
	}
122
123
	/**
124
	 * show update nofication row -- needed for multisite subsites, because WP won't tell you otherwise!
125
	 *
126
	 * @param string  $file
127
	 * @param array   $plugin
128
	 */
129
	public function show_update_notification( $file, $plugin ) {
130
131
		if ( is_network_admin() ) {
132
			return;
133
		}
134
135
		if( ! current_user_can( 'update_plugins' ) ) {
136
			return;
137
		}
138
139
		if( ! is_multisite() ) {
140
			return;
141
		}
142
143
		if ( $this->name != $file ) {
144
			return;
145
		}
146
147
		// Remove our filter on the site transient
148
		remove_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ), 10 );
149
150
		$update_cache = get_site_transient( 'update_plugins' );
151
152
		$update_cache = is_object( $update_cache ) ? $update_cache : new stdClass();
153
154
		if ( empty( $update_cache->response ) || empty( $update_cache->response[ $this->name ] ) ) {
155
156
			$version_info = $this->get_cached_version_info();
157
158
			if ( false === $version_info ) {
159
				$version_info = $this->api_request( 'plugin_latest_version', array( 'slug' => $this->slug, 'beta' => $this->beta ) );
160
161
				$this->set_version_info_cache( $version_info );
162
			}
163
164
			if ( ! is_object( $version_info ) ) {
165
				return;
166
			}
167
168
			if ( version_compare( $this->version, $version_info->new_version, '<' ) ) {
169
170
				$update_cache->response[ $this->name ] = $version_info;
171
172
			}
173
174
			$update_cache->last_checked = current_time( 'timestamp' );
175
			$update_cache->checked[ $this->name ] = $this->version;
176
177
			set_site_transient( 'update_plugins', $update_cache );
178
179
		} else {
180
181
			$version_info = $update_cache->response[ $this->name ];
182
183
		}
184
185
		// Restore our filter
186
		add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
187
188
		if ( ! empty( $update_cache->response[ $this->name ] ) && version_compare( $this->version, $version_info->new_version, '<' ) ) {
189
190
			// build a plugin list row, with update notification
191
			$wp_list_table = _get_list_table( 'WP_Plugins_List_Table' );
192
			# <tr class="plugin-update-tr"><td colspan="' . $wp_list_table->get_column_count() . '" class="plugin-update colspanchange">
193
			echo '<tr class="plugin-update-tr" id="' . $this->slug . '-update" data-slug="' . $this->slug . '" data-plugin="' . $this->slug . '/' . $file . '">';
194
			echo '<td colspan="3" class="plugin-update colspanchange">';
195
			echo '<div class="update-message notice inline notice-warning notice-alt">';
196
197
			$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' );
198
199
			if ( empty( $version_info->download_link ) ) {
200
				printf(
201
					__( 'There is a new version of %1$s available. %2$sView version %3$s details%4$s.', 'easy-digital-downloads' ),
202
					esc_html( $version_info->name ),
203
					'<a target="_blank" class="thickbox" href="' . esc_url( $changelog_link ) . '">',
204
					esc_html( $version_info->new_version ),
205
					'</a>'
206
				);
207
			} else {
208
				printf(
209
					__( 'There is a new version of %1$s available. %2$sView version %3$s details%4$s or %5$supdate now%6$s.', 'easy-digital-downloads' ),
210
					esc_html( $version_info->name ),
211
					'<a target="_blank" class="thickbox" href="' . esc_url( $changelog_link ) . '">',
212
					esc_html( $version_info->new_version ),
213
					'</a>',
214
					'<a href="' . esc_url( wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $this->name, 'upgrade-plugin_' . $this->name ) ) .'">',
215
					'</a>'
216
				);
217
			}
218
219
			do_action( "in_plugin_update_message-{$file}", $plugin, $version_info );
220
221
			echo '</div></td></tr>';
222
		}
223
	}
224
225
	/**
226
	 * Updates information on the "View version x.x details" page with custom data.
227
	 *
228
	 * @uses api_request()
229
	 *
230
	 * @param mixed   $_data
231
	 * @param string  $_action
232
	 * @param object  $_args
233
	 * @return object $_data
234
	 */
235
	public function plugins_api_filter( $_data, $_action = '', $_args = null ) {
236
237
		if ( $_action != 'plugin_information' ) {
238
239
			return $_data;
240
241
		}
242
243
		if ( ! isset( $_args->slug ) || ( $_args->slug != $this->slug ) ) {
244
245
			return $_data;
246
247
		}
248
249
		$to_send = array(
250
			'slug'   => $this->slug,
251
			'is_ssl' => is_ssl(),
252
			'fields' => array(
253
				'banners' => array(),
254
				'reviews' => false
255
			)
256
		);
257
258
		$cache_key = 'edd_api_request_' . md5( serialize( $this->slug . $this->api_data['license'] . $this->beta ) );
259
260
		// Get the transient where we store the api request for this plugin for 24 hours
261
		$edd_api_request_transient = $this->get_cached_version_info( $cache_key );
262
263
		//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.
264
		if ( empty( $edd_api_request_transient ) ) {
265
266
			$api_response = $this->api_request( 'plugin_information', $to_send );
267
268
			// Expires in 3 hours
269
			$this->set_version_info_cache( $api_response, $cache_key );
270
271
			if ( false !== $api_response ) {
272
				$_data = $api_response;
273
			}
274
275
		} else {
276
			$_data = $edd_api_request_transient;
277
		}
278
279
		// Convert sections into an associative array, since we're getting an object, but Core expects an array.
280
		if ( isset( $_data->sections ) && ! is_array( $_data->sections ) ) {
281
			$new_sections = array();
282
			foreach ( $_data->sections as $key => $key ) {
283
				$new_sections[ $key ] = $key;
284
			}
285
286
			$_data->sections = $new_sections;
287
		}
288
289
		// Convert banners into an associative array, since we're getting an object, but Core expects an array.
290
		if ( isset( $_data->banners ) && ! is_array( $_data->banners ) ) {
291
			$new_banners = array();
292
			foreach ( $_data->banners as $key => $key ) {
293
				$new_banners[ $key ] = $key;
294
			}
295
296
			$_data->banners = $new_banners;
297
		}
298
299
		return $_data;
300
	}
301
302
	/**
303
	 * Disable SSL verification in order to prevent download update failures
304
	 *
305
	 * @param array   $args
306
	 * @param string  $url
307
	 * @return object $array
0 ignored issues
show
Documentation introduced by
Should the return type not be array?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
308
	 */
309
	public function http_request_args( $args, $url ) {
310
		// If it is an https request and we are performing a package download, disable ssl verification
311
		if ( strpos( $url, 'https://' ) !== false && strpos( $url, 'edd_action=package_download' ) ) {
312
			$args['sslverify'] = false;
313
		}
314
		return $args;
315
	}
316
317
	/**
318
	 * Calls the API and, if successfull, returns the object delivered by the API.
319
	 *
320
	 * @uses get_bloginfo()
321
	 * @uses wp_remote_post()
322
	 * @uses is_wp_error()
323
	 *
324
	 * @param string  $_action The requested action.
325
	 * @param array   $_data   Parameters for the API action.
326
	 * @return false|object
327
	 */
328
	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...
329
330
		global $wp_version;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
331
332
		$data = array_merge( $this->api_data, $_data );
333
334
		if ( $data['slug'] != $this->slug ) {
335
			return;
336
		}
337
338
		if( $this->api_url == trailingslashit (home_url() ) ) {
339
			return false; // Don't allow a plugin to ping itself
340
		}
341
342
		$api_params = array(
343
			'edd_action' => 'get_version',
344
			'license'    => ! empty( $data['license'] ) ? $data['license'] : '',
345
			'item_name'  => isset( $data['item_name'] ) ? $data['item_name'] : false,
346
			'item_id'    => isset( $data['item_id'] ) ? $data['item_id'] : false,
347
			'version'    => isset( $data['version'] ) ? $data['version'] : false,
348
			'slug'       => $data['slug'],
349
			'author'     => $data['author'],
350
			'url'        => home_url(),
351
			'beta'       => ! empty( $data['beta'] ),
352
		);
353
354
		$request = wp_remote_post( $this->api_url, array( 'timeout' => 15, 'sslverify' => false, 'body' => $api_params ) );
355
356
		if ( ! is_wp_error( $request ) ) {
357
			$request = json_decode( wp_remote_retrieve_body( $request ) );
358
		}
359
360
		if ( $request && isset( $request->sections ) ) {
361
			$request->sections = maybe_unserialize( $request->sections );
362
		} else {
363
			$request = false;
364
		}
365
366
		if ( $request && isset( $request->banners ) ) {
367
			$request->banners = maybe_unserialize( $request->banners );
368
		}
369
370
		if( ! empty( $request->sections ) ) {
371
			foreach( $request->sections as $key => $section ) {
372
				$request->$key = (array) $section;
373
			}
374
		}
375
376
		return $request;
377
	}
378
379
	public function show_changelog() {
380
381
		global $edd_plugin_data;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
382
383
		if( empty( $_REQUEST['edd_sl_action'] ) || 'view_plugin_changelog' != $_REQUEST['edd_sl_action'] ) {
384
			return;
385
		}
386
387
		if( empty( $_REQUEST['plugin'] ) ) {
388
			return;
389
		}
390
391
		if( empty( $_REQUEST['slug'] ) ) {
392
			return;
393
		}
394
395
		if( ! current_user_can( 'update_plugins' ) ) {
396
			wp_die( __( 'You do not have permission to install plugin updates', 'easy-digital-downloads' ), __( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
397
		}
398
399
		$data         = $edd_plugin_data[ $_REQUEST['slug'] ];
400
		$beta         = ! empty( $data['beta'] ) ? true : false;
401
		$cache_key    = md5( 'edd_plugin_' . sanitize_key( $_REQUEST['plugin'] ) . '_' . $beta . '_version_info' );
402
		$version_info = $this->get_cached_version_info( $cache_key );
403
404
		if( false === $version_info ) {
405
406
			$api_params = array(
407
				'edd_action' => 'get_version',
408
				'item_name'  => isset( $data['item_name'] ) ? $data['item_name'] : false,
409
				'item_id'    => isset( $data['item_id'] ) ? $data['item_id'] : false,
410
				'slug'       => $_REQUEST['slug'],
411
				'author'     => $data['author'],
412
				'url'        => home_url(),
413
				'beta'       => ! empty( $data['beta'] )
414
			);
415
416
			$request = wp_remote_post( $this->api_url, array( 'timeout' => 15, 'sslverify' => false, 'body' => $api_params ) );
417
418
			if ( ! is_wp_error( $request ) ) {
419
				$version_info = json_decode( wp_remote_retrieve_body( $request ) );
420
			}
421
422
423
			if ( ! empty( $version_info ) && isset( $version_info->sections ) ) {
424
				$version_info->sections = maybe_unserialize( $version_info->sections );
425
			} else {
426
				$version_info = false;
427
			}
428
429
			if( ! empty( $version_info ) ) {
430
				foreach( $version_info->sections as $key => $section ) {
431
					$version_info->$key = (array) $section;
432
				}
433
			}
434
435
			$this->set_version_info_cache( $version_info, $cache_key );
436
437
		}
438
439
		if( ! empty( $version_info ) && isset( $version_info->sections['changelog'] ) ) {
440
			echo '<div style="background:#fff;padding:10px;">' . $version_info->sections['changelog'] . '</div>';
441
		}
442
443
		exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method show_changelog() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
444
	}
445
446
	public function get_cached_version_info( $cache_key = '' ) {
447
448
		if( empty( $cache_key ) ) {
449
			$cache_key = $this->cache_key;
450
		}
451
452
		$cache = get_option( $cache_key );
453
454
		if( empty( $cache['timeout'] ) || current_time( 'timestamp' ) > $cache['timeout'] ) {
455
			return false; // Cache is expired
456
		}
457
458
		return json_decode( $cache['value'] );
459
460
	}
461
462
	public function set_version_info_cache( $value = '', $cache_key = '' ) {
463
464
		if( empty( $cache_key ) ) {
465
			$cache_key = $this->cache_key;
466
		}
467
468
		$data = array(
469
			'timeout' => strtotime( '+3 hours', current_time( 'timestamp' ) ),
470
			'value'   => json_encode( $value )
471
		);
472
473
		update_option( $cache_key, $data );
474
475
	}
476
477
}