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