PluginProbe ʕ •ᴥ•ʔ
ShopEngine Elementor WooCommerce Builder Addon – All in One WooCommerce Solution / 3.1.0
ShopEngine Elementor WooCommerce Builder Addon – All in One WooCommerce Solution v3.1.0
4.9.2 4.9.1 4.9.0 2.0.0 2.1.0 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.1 3.0.0 3.1.0 3.1.1 4.0.0 4.0.1 4.1.0 4.1.1 4.2.0 4.2.1 4.3.0 4.3.1 4.4.0 4.5.0 4.5.1 4.6.0 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.6.6 4.6.7 4.6.8 4.6.9 4.7.0 4.7.1 4.7.2 4.7.3 4.7.4 4.7.5 4.7.6 4.7.7 4.7.8 4.7.9 4.8.0 4.8.1 4.8.2 4.8.3 4.8.4 4.8.5 4.8.6 4.8.7 4.8.8 4.8.9 trunk 0.1.2-beta 0.1.3-beta 0.1.4-beta 1.0.0 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.2.1 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.4.0 1.4.1 1.5.0 1.5.1 1.6.0 1.6.1 1.7.0 1.8.0 1.8.1 1.9.0
shopengine / libs / updater / plugin-updater.php
shopengine / libs / updater Last commit date
init.php 3 years ago plugin-updater.php 3 years ago
plugin-updater.php
575 lines
1 <?php
2
3 namespace ShopEngine\Libs\Updater;
4
5 use ShopEngine\Utils\Helper;
6
7 defined('ABSPATH') || exit;
8
9 /**
10 * Allows plugins to use their own update API.
11 *
12 * @version 1.1.4
13 */
14 class Plugin_Updater {
15
16 private $api_url = '';
17 private $api_data = [];
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;
25 private $update_check_interval = '5 hours';
26
27 /**
28 * Class constructor.
29 *
30 * @param string $_api_url The URL pointing to the custom API endpoint.
31 * @param string $_plugin_file Path to the plugin file.
32 * @param array $_api_data Optional data to send with API calls.
33 * @uses hook()
34 *
35 * @uses plugin_basename()
36 */
37 public function __construct($_api_url, $_plugin_file, $_api_data = null) {
38
39 global $edd_plugin_data;
40
41 $this->api_url = trailingslashit($_api_url);
42 $this->api_data = $_api_data;
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 * @param array $edd_plugin_data Array of EDD SL plugin data.
56 * @since 1.0.0
57 *
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 * Set up WordPress filters to hook into WP's update process.
67 *
68 * @return void
69 * @uses add_filter()
70 *
71 */
72 public function init() {
73
74 add_filter('pre_set_site_transient_update_plugins', [$this, 'check_update']);
75 add_filter('plugins_api', [$this, 'plugins_api_filter'], 10, 3);
76 remove_action('after_plugin_row_' . $this->name, 'wp_plugin_update_row', 10);
77 add_action('after_plugin_row_' . $this->name, [$this, 'show_update_notification'], 10, 2);
78 add_action('admin_init', [$this, 'show_changelog']);
79 }
80
81 /**
82 * Check for Updates at the defined API endpoint and modify the update array.
83 *
84 * This function dives into the update API just when WordPress creates its update array,
85 * then adds a custom API call and injects the custom plugin data retrieved from the API.
86 * It is reassembled from parts of the native WordPress plugin update code.
87 * See wp-includes/update.php line 121 for the original wp_update_plugins() function.
88 *
89 * @param array $_transient_data Update array build by WordPress.
90 * @return array Modified update array with custom plugin data.
91 * @uses api_request()
92 *
93 */
94 public function check_update($_transient_data) {
95
96 global $pagenow;
97
98 if(!is_object($_transient_data)) {
99 $_transient_data = new \stdClass;
100 }
101
102 if('plugins.php' == $pagenow && is_multisite()) {
103 return $_transient_data;
104 }
105
106 if(!empty($_transient_data->response) && !empty($_transient_data->response[$this->name]) && false === $this->wp_override) {
107 return $_transient_data;
108 }
109
110 $version_info = $this->get_cached_version_info();
111
112 if(false === $version_info) {
113 $version_info = $this->api_request('plugin_latest_version', ['slug' => $this->slug, 'beta' => $this->beta]);
114
115 $this->set_version_info_cache($version_info);
116
117 }
118
119 if(false !== $version_info && is_object($version_info) && isset($version_info->new_version)) {
120
121 if(version_compare($this->version, $version_info->new_version, '<')) {
122
123 $_transient_data->response[$this->name] = $version_info;
124
125 // Make sure the plugin property is set to the plugin's name/location. See issue 1463 on Software Licensing's GitHub repo.
126 $_transient_data->response[$this->name]->plugin = $this->name;
127
128 }
129
130 $_transient_data->last_checked = time();
131 $_transient_data->checked[$this->name] = $this->version;
132
133 }
134
135 return $_transient_data;
136 }
137
138 /**
139 * show update nofication row -- needed for multisite subsites, because WP won't tell you otherwise!
140 *
141 * @param string $file
142 * @param array $plugin
143 */
144 public function show_update_notification($file, $plugin) {
145
146 if(is_network_admin()) {
147 return;
148 }
149
150 if(!current_user_can('update_plugins')) {
151 return;
152 }
153
154 if(!is_multisite()) {
155 return;
156 }
157
158 if($this->name != $file) {
159 return;
160 }
161
162 // Remove our filter on the site transient
163 remove_filter('pre_set_site_transient_update_plugins', [$this, 'check_update'], 10);
164
165 $update_cache = get_site_transient('update_plugins');
166
167 $update_cache = is_object($update_cache) ? $update_cache : new \stdClass();
168
169 if(empty($update_cache->response) || empty($update_cache->response[$this->name])) {
170
171 $version_info = $this->get_cached_version_info();
172
173 if(false === $version_info) {
174 $version_info = $this->api_request('plugin_latest_version', ['slug' => $this->slug, 'beta' => $this->beta]);
175
176 // Since we disabled our filter for the transient, we aren't running our object conversion on banners, sections, or icons. Do this now:
177 if(isset($version_info->banners) && !is_array($version_info->banners)) {
178 $version_info->banners = $this->convert_object_to_array($version_info->banners);
179 }
180
181 if(isset($version_info->sections) && !is_array($version_info->sections)) {
182 $version_info->sections = $this->convert_object_to_array($version_info->sections);
183 }
184
185 if(isset($version_info->icons) && !is_array($version_info->icons)) {
186 $version_info->icons = $this->convert_object_to_array($version_info->icons);
187 }
188
189 $this->set_version_info_cache($version_info);
190 }
191
192 if(!is_object($version_info)) {
193 return;
194 }
195
196 if(version_compare($this->version, $version_info->new_version, '<')) {
197
198 $update_cache->response[$this->name] = $version_info;
199
200 }
201
202 $update_cache->last_checked = time();
203 $update_cache->checked[$this->name] = $this->version;
204
205 set_site_transient('update_plugins', $update_cache);
206
207 } else {
208
209 $version_info = $update_cache->response[$this->name];
210
211 }
212
213 // Restore our filter
214 add_filter('pre_set_site_transient_update_plugins', [$this, 'check_update']);
215
216 if(!empty($update_cache->response[$this->name]) && version_compare($this->version, $version_info->new_version, '<')) {
217
218 // build a plugin list row, with update notification
219 // $wp_list_table = _get_list_table('WP_Plugins_List_Table');
220 # <tr class="plugin-update-tr"><td colspan="' . $wp_list_table->get_column_count() . '" class="plugin-update colspanchange">
221
222 echo '<tr class="plugin-update-tr" id="' . esc_attr($this->slug) . '-update" data-slug="' . esc_attr($this->slug) . '" data-plugin="' . esc_attr($this->slug) . '/' . esc_attr($file) . '">';
223 echo '<td colspan="3" class="plugin-update colspanchange">';
224 echo '<div class="update-message notice inline notice-warning notice-alt">';
225
226 $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');
227
228 if(empty($version_info->download_link)) {
229 printf(
230 esc_html__('There is a new version of %1$s available. %2$sView version %3$s details%4$s.', 'shopengine'),
231 esc_html($version_info->name),
232 '<a title="' . esc_html__("Latest Version Changelog","shopengine") . '" target="_blank" class="thickbox" rel="noopener" href="' . esc_url($changelog_link) . '">',
233 esc_html($version_info->new_version),
234 '</a>'
235 );
236 } else {
237 printf(
238 esc_html__('There is a new version of %1$s available. %2$sView version %3$s details%4$s or %5$supdate now%6$s.', 'shopengine'),
239 esc_html($version_info->name),
240 '<a title="' . esc_html__("Latest Version Changelog","shopengine") . '" target="_blank" rel="noopener" class="thickbox" href="' . esc_url($changelog_link) . '">',
241 esc_html($version_info->new_version),
242 '</a>',
243 '<a title="' . esc_html__("Latest Version Changelog","shopengine") . '" href="' . esc_url(wp_nonce_url(self_admin_url('update.php?action=upgrade-plugin&plugin=') . $this->name, 'upgrade-plugin_' . $this->name)) . '">',
244 '</a>'
245 );
246 }
247
248 do_action("in_plugin_update_message-{$file}", $plugin, $version_info);
249
250 echo '</div></td></tr>';
251 }
252 }
253
254 /**
255 * Updates information on the "View version x.x details" page with custom data.
256 *
257 * @param mixed $_data
258 * @param string $_action
259 * @param object $_args
260 * @return object $_data
261 * @uses api_request()
262 *
263 */
264 public function plugins_api_filter($_data, $_action = '', $_args = null) {
265
266 if($_action != 'plugin_information') {
267
268 return $_data;
269
270 }
271
272 if(!isset($_args->slug) || ($_args->slug != $this->slug)) {
273
274 return $_data;
275
276 }
277
278 $to_send = [
279 'slug' => $this->slug,
280 'is_ssl' => is_ssl(),
281 'fields' => [
282 'banners' => [],
283 'reviews' => false,
284 'icons' => [],
285 ],
286 ];
287
288 $cache_key = 'edd_api_request_' . md5(serialize($this->slug . $this->api_data['license'] . $this->beta));
289
290 // Get the transient where we store the api request for this plugin for 24 hours
291 $edd_api_request_transient = $this->get_cached_version_info($cache_key);
292
293 //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.
294 if(empty($edd_api_request_transient)) {
295
296 $api_response = $this->api_request('plugin_information', $to_send);
297
298 // Expires in 3 hours
299 $this->set_version_info_cache($api_response, $cache_key);
300
301 if(false !== $api_response) {
302 $_data = $api_response;
303 }
304
305 } else {
306 $_data = $edd_api_request_transient;
307 }
308
309 // Convert sections into an associative array, since we're getting an object, but Core expects an array.
310 if(isset($_data->sections) && !is_array($_data->sections)) {
311 $_data->sections = $this->convert_object_to_array($_data->sections);
312 }
313
314 // Convert banners into an associative array, since we're getting an object, but Core expects an array.
315 if(isset($_data->banners) && !is_array($_data->banners)) {
316 $_data->banners = $this->convert_object_to_array($_data->banners);
317 }
318
319 // Convert icons into an associative array, since we're getting an object, but Core expects an array.
320 if(isset($_data->icons) && !is_array($_data->icons)) {
321 $_data->icons = $this->convert_object_to_array($_data->icons);
322 }
323
324 if(!isset($_data->plugin)) {
325 $_data->plugin = $this->name;
326 }
327
328 return $_data;
329 }
330
331 /**
332 * Convert some objects to arrays when injecting data into the update API
333 *
334 * Some data like sections, banners, and icons are expected to be an associative array, however due to the JSON
335 * decoding, they are objects. This method allows us to pass in the object and return an associative array.
336 *
337 * @param stdClass $data
338 *
339 * @return array
340 * @since 3.6.5
341 *
342 */
343 private function convert_object_to_array($data) {
344 $new_data = [];
345
346 foreach($data as $key => $value) {
347 $new_data[$key] = $value;
348 }
349
350 return $new_data;
351 }
352
353 /**
354 * Disable SSL verification in order to prevent download update failures
355 *
356 * @param array $args
357 * @param string $url
358 * @return object $array
359 */
360 public function http_request_args($args, $url) {
361
362 $verify_ssl = $this->verify_ssl();
363 if(strpos($url, 'https://') !== false && strpos($url, 'edd_action=package_download')) {
364 $args['sslverify'] = $verify_ssl;
365 }
366
367 return $args;
368 }
369
370 /**
371 * Calls the API and, if successfull, returns the object delivered by the API.
372 *
373 * @param string $_action The requested action.
374 * @param array $_data Parameters for the API action.
375 * @return false|object
376 * @uses get_bloginfo()
377 * @uses wp_remote_post()
378 * @uses is_wp_error()
379 *
380 */
381 private function api_request($_action, $_data) {
382
383 global $wp_version, $edd_plugin_url_available;
384
385 $verify_ssl = $this->verify_ssl();
386
387 // Do a quick status check on this domain if we haven't already checked it.
388 $store_hash = md5($this->api_url);
389 if(!is_array($edd_plugin_url_available) || !isset($edd_plugin_url_available[$store_hash])) {
390 $test_url_parts = parse_url($this->api_url);
391
392 $scheme = !empty($test_url_parts['scheme']) ? $test_url_parts['scheme'] : 'http';
393 $host = !empty($test_url_parts['host']) ? $test_url_parts['host'] : '';
394 $port = !empty($test_url_parts['port']) ? ':' . $test_url_parts['port'] : '';
395
396 if(empty($host)) {
397 $edd_plugin_url_available[$store_hash] = false;
398 } else {
399 $test_url = $scheme . '://' . $host . $port;
400 $response = wp_remote_get($test_url, ['timeout' => $this->health_check_timeout, 'sslverify' => $verify_ssl]);
401 $edd_plugin_url_available[$store_hash] = is_wp_error($response) ? false : true;
402 }
403 }
404
405 if(false === $edd_plugin_url_available[$store_hash]) {
406 return;
407 }
408
409 $data = array_merge($this->api_data, $_data);
410
411 if($data['slug'] != $this->slug) {
412 return;
413 }
414
415 if($this->api_url == trailingslashit(home_url())) {
416 return false; // Don't allow a plugin to ping itself
417 }
418
419 $api_params = [
420 'edd_action' => 'get_version',
421 'license' => !empty($data['license']) ? $data['license'] : '',
422 'item_name' => isset($data['item_name']) ? $data['item_name'] : false,
423 'item_id' => isset($data['item_id']) ? $data['item_id'] : false,
424 'version' => isset($data['version']) ? $data['version'] : false,
425 'slug' => $data['slug'],
426 'author' => $data['author'],
427 'url' => home_url(),
428 'beta' => !empty($data['beta']),
429 ];
430
431 $request = wp_remote_post($this->api_url, ['timeout' => 15, 'sslverify' => $verify_ssl, 'body' => $api_params]);
432
433 if(!is_wp_error($request)) {
434 $request = json_decode(wp_remote_retrieve_body($request));
435 }
436
437 if($request && isset($request->sections)) {
438 $request->sections = maybe_unserialize($request->sections);
439 } else {
440 $request = false;
441 }
442
443 if($request && isset($request->banners)) {
444 $request->banners = maybe_unserialize($request->banners);
445 }
446
447 if($request && isset($request->icons)) {
448 $request->icons = maybe_unserialize($request->icons);
449 }
450
451 if(!empty($request->sections)) {
452 foreach($request->sections as $key => $section) {
453 $request->$key = (array)$section;
454 }
455 }
456
457 return $request;
458 }
459
460 public function show_changelog() {
461
462 global $edd_plugin_data;
463 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This hook can access only admin and not possible verify nonce here
464 if(empty($_REQUEST['edd_sl_action']) || 'view_plugin_changelog' != $_REQUEST['edd_sl_action']) {
465 return;
466 }
467 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This hook can access only admin and not possible verify nonce here
468 if(empty($_REQUEST['plugin'])) {
469 return;
470 }
471 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This hook can access only admin and not possible verify nonce here
472 if(empty($_REQUEST['slug'])) {
473 return;
474 }
475
476 if(!current_user_can('update_plugins')) {
477 wp_die(esc_html__('You do not have permission to install plugin updates', 'shopengine'), esc_html__('Error', 'shopengine'), ['response' => 403]);
478 }
479 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This hook can access only admin and not possible verify nonce here
480 $data = $edd_plugin_data[sanitize_text_field(wp_unslash($_REQUEST['slug']))];
481 $beta = !empty($data['beta']) ? true : false;
482 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This hook can access only admin and not possible verify nonce here
483 $cache_key = md5('edd_plugin_' . sanitize_key($_REQUEST['plugin']) . '_' . $beta . '_version_info');
484 $version_info = $this->get_cached_version_info($cache_key);
485
486 if(false === $version_info) {
487
488 $api_params = [
489 'edd_action' => 'get_version',
490 'item_name' => isset($data['item_name']) ? $data['item_name'] : false,
491 'item_id' => isset($data['item_id']) ? $data['item_id'] : false,
492 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This hook can access only admin and not possible verify nonce here
493 'slug' => sanitize_text_field(wp_unslash($_REQUEST['slug'])),
494 'author' => $data['author'],
495 'url' => home_url(),
496 'beta' => !empty($data['beta']),
497 ];
498
499 $verify_ssl = $this->verify_ssl();
500 $request = wp_remote_post($this->api_url, ['timeout' => 15, 'sslverify' => $verify_ssl, 'body' => $api_params]);
501
502 if(!is_wp_error($request)) {
503 $version_info = json_decode(wp_remote_retrieve_body($request));
504 }
505
506
507 if(!empty($version_info) && isset($version_info->sections)) {
508 $version_info->sections = maybe_unserialize($version_info->sections);
509 } else {
510 $version_info = false;
511 }
512
513 if(!empty($version_info)) {
514 foreach($version_info->sections as $key => $section) {
515 $version_info->$key = (array)$section;
516 }
517 }
518
519 $this->set_version_info_cache($version_info, $cache_key);
520
521 }
522
523 if(!empty($version_info) && isset($version_info->sections['changelog'])) {
524 echo '<div style="background:#fff;padding:10px;">' . wp_kses($version_info->sections['changelog'], Helper::get_kses_array()) . '</div>';
525 }
526
527 exit;
528 }
529
530 public function get_cached_version_info($cache_key = '') {
531
532 if(empty($cache_key)) {
533 $cache_key = $this->cache_key;
534 }
535
536 $cache = get_option($cache_key);
537
538 if(empty($cache['timeout']) || time() > $cache['timeout']) {
539 return false; // Cache is expired
540 }
541
542 // We need to turn the icons into an array, thanks to WP Core forcing these into an object at some point.
543 $cache['value'] = json_decode($cache['value']);
544 if(!empty($cache['value']->icons)) {
545 $cache['value']->icons = (array)$cache['value']->icons;
546 }
547
548 return $cache['value'];
549 }
550
551 public function set_version_info_cache($value = '', $cache_key = '') {
552
553 if(empty($cache_key)) {
554 $cache_key = $this->cache_key;
555 }
556
557 $data = [
558 'timeout' => strtotime($this->update_check_interval, time()),
559 'value' => json_encode($value),
560 ];
561
562 update_option($cache_key, $data, 'no');
563 }
564
565 /**
566 * Returns if the SSL of the store should be verified.
567 *
568 * @return bool
569 * @since 1.6.13
570 */
571 private function verify_ssl() {
572 return (bool)apply_filters('edd_sl_api_request_verify_ssl', true, $this);
573 }
574 }
575