PluginProbe
Floating Button – Easily Create Sticky, Fixed & Floating Buttons / 6.0.1
Floating Button – Easily Create Sticky, Fixed & Floating Buttons v6.0.1
trunk 2.0.2 3.0 4.0 4.1.2 4.3 5.0 5.0.1 5.1 5.1.1 5.2 5.3 5.3.1 6.0 6.0.1 6.0.10 6.0.11 6.0.12 6.0.13 6.0.2 6.0.3 6.0.4 6.0.5 6.0.6 6.0.7 All 31 releases
floating-button / classes / Update / Plugin_Updater.php

Plugin_Updater.php in Floating Button – Easily Create Sticky, Fixed & Floating Buttons 6.0.1, at classes/Update/Plugin_Updater.php

685 lines 19.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FloatingButton\Update;
4
5 defined( 'ABSPATH' ) || exit;
6
7 use stdClass;
8
9 class Plugin_Updater {
10
11 private $api_url = '';
12 private $api_data = array();
13 private $plugin_file = '';
14 private $name = '';
15 private $slug = '';
16 private $version = '';
17 private $wp_override = false;
18 private $beta = false;
19 private $failed_request_cache_key;
20
21 /**
22 * Class constructor.
23 *
24 * @param string $_api_url The URL pointing to the custom API endpoint.
25 * @param string $_plugin_file Path to the plugin file.
26 * @param array $_api_data Optional data to send with API calls.
27 *
28 * @uses hook()
29 *
30 * @uses plugin_basename()
31 */
32 public function __construct( $_api_url, $_plugin_file, $_api_data = null ) {
33
34 global $edd_plugin_data;
35
36 $this->api_url = trailingslashit( $_api_url );
37 $this->api_data = $_api_data;
38 $this->plugin_file = $_plugin_file;
39 $this->name = plugin_basename( $_plugin_file );
40 $this->slug = basename( $_plugin_file, '.php' );
41 $this->version = $_api_data['version'];
42 $this->wp_override = isset( $_api_data['wp_override'] ) ? (bool) $_api_data['wp_override'] : false;
43 $this->beta = ! empty( $this->api_data['beta'] ) ? true : false;
44 $this->failed_request_cache_key = 'edd_sl_failed_http_' . md5( $this->api_url );
45
46 $edd_plugin_data[ $this->slug ] = $this->api_data;
47
48 /**
49 * Fires after the $edd_plugin_data is setup.
50 *
51 * @param array $edd_plugin_data Array of EDD SL plugin data.
52 *
53 * @since x.x.x
54 *
55 */
56 do_action( 'post_edd_sl_plugin_updater_setup', $edd_plugin_data );
57
58 // Set up hooks.
59 $this->init();
60
61 }
62
63 /**
64 * Set up WordPress filters to hook into WP's update process.
65 *
66 * @return void
67 * @uses add_filter()
68 *
69 */
70 public function init() {
71
72 add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
73 add_filter( 'plugins_api', array( $this, 'plugins_api_filter' ), 10, 3 );
74 add_action( 'after_plugin_row', array( $this, 'show_update_notification' ), 10, 2 );
75 add_action( 'admin_init', array( $this, 'show_changelog' ) );
76
77 }
78
79 /**
80 * Check for Updates at the defined API endpoint and modify the update array.
81 *
82 * This function dives into the update API just when WordPress creates its update array,
83 * then adds a custom API call and injects the custom plugin data retrieved from the API.
84 * It is reassembled from parts of the native WordPress plugin update code.
85 * See wp-includes/update.php line 121 for the original wp_update_plugins() function.
86 *
87 * @param array $_transient_data Update array build by WordPress.
88 *
89 * @return array Modified update array with custom plugin data.
90 * @uses api_request()
91 *
92 */
93 public function check_update( $_transient_data ) {
94
95 global $pagenow;
96
97 if ( ! is_object( $_transient_data ) ) {
98 $_transient_data = new stdClass();
99 }
100
101 if ( ! empty( $_transient_data->response ) && ! empty( $_transient_data->response[ $this->name ] ) && false === $this->wp_override ) {
102 return $_transient_data;
103 }
104
105 $current = $this->get_repo_api_data();
106 if ( false !== $current && is_object( $current ) && isset( $current->new_version ) ) {
107 if ( version_compare( $this->version, $current->new_version, '<' ) ) {
108 $_transient_data->response[ $this->name ] = $current;
109 } else {
110 // Populating the no_update information is required to support auto-updates in WordPress 5.5.
111 $_transient_data->no_update[ $this->name ] = $current;
112 }
113 }
114 $_transient_data->last_checked = time();
115 $_transient_data->checked[ $this->name ] = $this->version;
116
117 return $_transient_data;
118 }
119
120 /**
121 * Get repo API data from store.
122 * Save to cache.
123 *
124 * @return \stdClass
125 */
126 public function get_repo_api_data() {
127 $version_info = $this->get_cached_version_info();
128
129 if ( false === $version_info ) {
130
131 $version_info = $this->api_request(
132 'plugin_latest_version',
133 array(
134 'slug' => $this->slug,
135 'beta' => $this->beta,
136 )
137 );
138 if ( ! $version_info ) {
139 return false;
140 }
141
142 // This is required for your plugin to support auto-updates in WordPress 5.5.
143 $version_info->plugin = $this->name;
144 $version_info->id = $this->name;
145 $version_info->tested = $this->get_tested_version( $version_info );
146
147 $this->set_version_info_cache( $version_info );
148 }
149
150 return $version_info;
151 }
152
153 /**
154 * Gets the plugin's tested version.
155 *
156 * @param object $version_info
157 *
158 * @return null|string
159 * @since 1.9.2
160 */
161 private function get_tested_version( $version_info ) {
162
163 // There is no tested version.
164 if ( empty( $version_info->tested ) ) {
165 return null;
166 }
167
168 // Strip off extra version data so the result is x.y or x.y.z.
169 list( $current_wp_version ) = explode( '-', get_bloginfo( 'version' ) );
170
171 // The tested version is greater than or equal to the current WP version, no need to do anything.
172 if ( version_compare( $version_info->tested, $current_wp_version, '>=' ) ) {
173 return $version_info->tested;
174 }
175 $current_version_parts = explode( '.', $current_wp_version );
176 $tested_parts = explode( '.', $version_info->tested );
177
178 // The current WordPress version is x.y.z, so update the tested version to match it.
179 if ( isset( $current_version_parts[2] ) && $current_version_parts[0] === $tested_parts[0] && $current_version_parts[1] === $tested_parts[1] ) {
180 $tested_parts[2] = $current_version_parts[2];
181 }
182
183 return implode( '.', $tested_parts );
184 }
185
186 /**
187 * Show the update notification on multisite subsites.
188 *
189 * @param string $file
190 * @param array $plugin
191 */
192 public function show_update_notification( $file, $plugin ) {
193
194 // Return early if in the network admin, or if this is not a multisite install.
195 if ( is_network_admin() || ! is_multisite() ) {
196 return;
197 }
198
199 // Allow single site admins to see that an update is available.
200 if ( ! current_user_can( 'activate_plugins' ) ) {
201 return;
202 }
203
204 if ( $this->name !== $file ) {
205 return;
206 }
207
208 // Do not print any message if update does not exist.
209 $update_cache = get_site_transient( 'update_plugins' );
210
211 if ( ! isset( $update_cache->response[ $this->name ] ) ) {
212 if ( ! is_object( $update_cache ) ) {
213 $update_cache = new stdClass();
214 }
215 $update_cache->response[ $this->name ] = $this->get_repo_api_data();
216 }
217
218 // Return early if this plugin isn't in the transient->response or if the site is running the current or newer version of the plugin.
219 if ( empty( $update_cache->response[ $this->name ] ) || version_compare( $this->version, $update_cache->response[ $this->name ]->new_version, '>=' ) ) {
220 return;
221 }
222
223 printf(
224 '<tr class="plugin-update-tr %3$s" id="%1$s-update" data-slug="%1$s" data-plugin="%2$s">',
225 $this->slug,
226 $file,
227 in_array( $this->name, $this->get_active_plugins(), true ) ? 'active' : 'inactive'
228 );
229
230 echo '<td colspan="3" class="plugin-update colspanchange">';
231 echo '<div class="update-message notice inline notice-warning notice-alt"><p>';
232
233 $changelog_link = '';
234 if ( ! empty( $update_cache->response[ $this->name ]->sections->changelog ) ) {
235 $changelog_link = add_query_arg(
236 array(
237 'edd_sl_action' => 'view_plugin_changelog',
238 'plugin' => urlencode( $this->name ),
239 'slug' => urlencode( $this->slug ),
240 'TB_iframe' => 'true',
241 'width' => 77,
242 'height' => 911,
243 ),
244 self_admin_url( 'index.php' )
245 );
246 }
247 $update_link = add_query_arg(
248 array(
249 'action' => 'upgrade-plugin',
250 'plugin' => urlencode( $this->name ),
251 ),
252 self_admin_url( 'update.php' )
253 );
254
255 printf(
256 /* translators: the plugin name. */
257 esc_html__( 'There is a new version of %1$s available.', 'easy-digital-downloads' ),
258 esc_html( $plugin['Name'] )
259 );
260
261 if ( ! current_user_can( 'update_plugins' ) ) {
262 echo ' ';
263 esc_html_e( 'Contact your network administrator to install the update.', 'easy-digital-downloads' );
264 } elseif ( empty( $update_cache->response[ $this->name ]->package ) && ! empty( $changelog_link ) ) {
265 echo ' ';
266 printf(
267 /* translators: 1. opening anchor tag, do not translate 2. the new plugin version 3. closing anchor tag, do not translate. */
268 __( '%1$sView version %2$s details%3$s.', 'easy-digital-downloads' ),
269 '<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">',
270 esc_html( $update_cache->response[ $this->name ]->new_version ),
271 '</a>'
272 );
273 } elseif ( ! empty( $changelog_link ) ) {
274 echo ' ';
275 printf(
276 __( '%1$sView version %2$s details%3$s or %4$supdate now%5$s.', 'easy-digital-downloads' ),
277 '<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">',
278 esc_html( $update_cache->response[ $this->name ]->new_version ),
279 '</a>',
280 '<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">',
281 '</a>'
282 );
283 } else {
284 printf(
285 ' %1$s%2$s%3$s',
286 '<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">',
287 esc_html__( 'Update now.', 'floating-button' ),
288 '</a>'
289 );
290 }
291
292 do_action( "in_plugin_update_message-{$file}", $plugin, $plugin );
293
294 echo '</p></div></td></tr>';
295 }
296
297 /**
298 * Gets the plugins active in a multisite network.
299 *
300 * @return array
301 */
302 private function get_active_plugins() {
303 $active_plugins = (array) get_option( 'active_plugins' );
304 $active_network_plugins = (array) get_site_option( 'active_sitewide_plugins' );
305
306 return array_merge( $active_plugins, array_keys( $active_network_plugins ) );
307 }
308
309 /**
310 * Updates information on the "View version x.x details" page with custom data.
311 *
312 * @param mixed $_data
313 * @param string $_action
314 * @param object $_args
315 *
316 * @return object $_data
317 * @uses api_request()
318 *
319 */
320 public function plugins_api_filter( $_data, $_action = '', $_args = null ) {
321
322
323 if ( 'plugin_information' !== $_action ) {
324
325 return $_data;
326
327 }
328
329 if ( ! isset( $_args->slug ) || ( $_args->slug !== $this->slug ) ) {
330
331 return $_data;
332
333 }
334
335 $to_send = array(
336 'slug' => $this->slug,
337 'is_ssl' => is_ssl(),
338 'fields' => array(
339 'banners' => array(),
340 'reviews' => false,
341 'icons' => array(),
342 ),
343 );
344
345 // Get the transient where we store the api request for this plugin for 24 hours
346 $edd_api_request_transient = $this->get_cached_version_info();
347
348 //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.
349 if ( empty( $edd_api_request_transient ) ) {
350
351 $api_response = $this->api_request( 'plugin_information', $to_send );
352
353
354 // Expires in 3 hours
355 $this->set_version_info_cache( $api_response );
356
357 if ( false !== $api_response ) {
358 $_data = $api_response;
359 }
360 } else {
361 $_data = $edd_api_request_transient;
362 }
363
364 // Convert sections into an associative array, since we're getting an object, but Core expects an array.
365 if ( isset( $_data->sections ) && ! is_array( $_data->sections ) ) {
366 $_data->sections = $this->convert_object_to_array( $_data->sections );
367 }
368
369 // Convert banners into an associative array, since we're getting an object, but Core expects an array.
370 if ( isset( $_data->banners ) && ! is_array( $_data->banners ) ) {
371 $_data->banners = $this->convert_object_to_array( $_data->banners );
372 }
373
374 // Convert icons into an associative array, since we're getting an object, but Core expects an array.
375 if ( isset( $_data->icons ) && ! is_array( $_data->icons ) ) {
376 $_data->icons = $this->convert_object_to_array( $_data->icons );
377 }
378
379 // Convert contributors into an associative array, since we're getting an object, but Core expects an array.
380 if ( isset( $_data->contributors ) && ! is_array( $_data->contributors ) ) {
381 $_data->contributors = $this->convert_object_to_array( $_data->contributors );
382 }
383
384 if ( ! isset( $_data->plugin ) ) {
385 $_data->plugin = $this->name;
386 }
387
388 return $_data;
389 }
390
391 /**
392 * Convert some objects to arrays when injecting data into the update API
393 *
394 * Some data like sections, banners, and icons are expected to be an associative array, however due to the JSON
395 * decoding, they are objects. This method allows us to pass in the object and return an associative array.
396 *
397 * @param stdClass $data
398 *
399 * @return array
400 * @since 3.6.5
401 *
402 */
403 private function convert_object_to_array( $data ) {
404 if ( ! is_array( $data ) && ! is_object( $data ) ) {
405 return array();
406 }
407 $new_data = array();
408 foreach ( $data as $key => $value ) {
409 $new_data[ $key ] = is_object( $value ) ? $this->convert_object_to_array( $value ) : $value;
410 }
411
412 return $new_data;
413 }
414
415 /**
416 * Disable SSL verification in order to prevent download update failures
417 *
418 * @param array $args
419 * @param string $url
420 *
421 * @return object $array
422 */
423 public function http_request_args( $args, $url ) {
424
425 if ( strpos( $url, 'https://' ) !== false && strpos( $url, 'edd_action=package_download' ) ) {
426 $args['sslverify'] = $this->verify_ssl();
427 }
428
429 return $args;
430
431 }
432
433 /**
434 * Calls the API and, if successfull, returns the object delivered by the API.
435 *
436 * @param string $_action The requested action.
437 * @param array $_data Parameters for the API action.
438 *
439 * @return false|object|void
440 * @uses get_bloginfo()
441 * @uses wp_remote_post()
442 * @uses is_wp_error()
443 *
444 */
445 private function api_request( $_action, $_data ) {
446 $data = array_merge( $this->api_data, $_data );
447
448 if ( $data['slug'] !== $this->slug ) {
449 return;
450 }
451
452 // Don't allow a plugin to ping itself
453 if ( trailingslashit( home_url() ) === $this->api_url ) {
454 return false;
455 }
456
457 if ( $this->request_recently_failed() ) {
458 return false;
459 }
460
461 return $this->get_version_from_remote();
462 }
463
464 /**
465 * Determines if a request has recently failed.
466 *
467 * @return bool
468 * @since 1.9.1
469 *
470 */
471 private function request_recently_failed() {
472 $failed_request_details = get_option( $this->failed_request_cache_key );
473
474 // Request has never failed.
475 if ( empty( $failed_request_details ) || ! is_numeric( $failed_request_details ) ) {
476 return false;
477 }
478
479 /*
480 * Request previously failed, but the timeout has expired.
481 * This means we're allowed to try again.
482 */
483 if ( time() > $failed_request_details ) {
484 delete_option( $this->failed_request_cache_key );
485
486 return false;
487 }
488
489 return true;
490 }
491
492 /**
493 * Logs a failed HTTP request for this API URL.
494 * We set a timestamp for 1 hour from now. This prevents future API requests from being
495 * made to this domain for 1 hour. Once the timestamp is in the past, API requests
496 * will be allowed again. This way if the site is down for some reason we don't bombard
497 * it with failed API requests.
498 *
499 * @see EDD_SL_Plugin_Updater::request_recently_failed
500 *
501 * @since 1.9.1
502 */
503 private function log_failed_request() {
504 update_option( $this->failed_request_cache_key, strtotime( '+1 hour' ) );
505 }
506
507 /**
508 * If available, show the changelog for sites in a multisite install.
509 */
510 public function show_changelog() {
511
512 if ( empty( $_REQUEST['edd_sl_action'] ) || 'view_plugin_changelog' !== $_REQUEST['edd_sl_action'] ) {
513 return;
514 }
515
516 if ( empty( $_REQUEST['plugin'] ) ) {
517 return;
518 }
519
520 if ( empty( $_REQUEST['slug'] ) || $this->slug !== $_REQUEST['slug'] ) {
521 return;
522 }
523
524 if ( ! current_user_can( 'update_plugins' ) ) {
525 wp_die( esc_html__( 'You do not have permission to install plugin updates', 'floating-button' ), esc_html__( 'Error', 'floating-button' ), array( 'response' => 403 ) );
526 }
527
528 $version_info = $this->get_repo_api_data();
529 if ( isset( $version_info->sections ) ) {
530 $sections = $this->convert_object_to_array( $version_info->sections );
531 if ( ! empty( $sections['changelog'] ) ) {
532 echo '<div style="background:#fff;padding:10px;">' . wp_kses_post( $sections['changelog'] ) . '</div>';
533 }
534 }
535
536 exit;
537 }
538
539 /**
540 * Gets the current version information from the remote site.
541 *
542 * @return array|false
543 */
544 private function get_version_from_remote() {
545 $api_params = array(
546 'edd_action' => 'get_version',
547 'license' => ! empty( $this->api_data['license'] ) ? $this->api_data['license'] : '',
548 'item_name' => isset( $this->api_data['item_name'] ) ? $this->api_data['item_name'] : false,
549 'item_id' => isset( $this->api_data['item_id'] ) ? $this->api_data['item_id'] : false,
550 'version' => isset( $this->api_data['version'] ) ? $this->api_data['version'] : false,
551 'slug' => $this->slug,
552 'author' => $this->api_data['author'],
553 'url' => home_url(),
554 'beta' => $this->beta,
555 'php_version' => phpversion(),
556 'wp_version' => get_bloginfo( 'version' ),
557 );
558
559 /**
560 * Filters the parameters sent in the API request.
561 *
562 * @param array $api_params The array of data sent in the request.
563 * @param array $this ->api_data The array of data set up in the class constructor.
564 * @param string $this ->plugin_file The full path and filename of the file.
565 */
566 $api_params = apply_filters( 'edd_sl_plugin_updater_api_params', $api_params, $this->api_data, $this->plugin_file );
567
568
569 $request = wp_remote_post(
570 $this->api_url,
571 array(
572 'timeout' => 15,
573 'sslverify' => $this->verify_ssl(),
574 'body' => $api_params,
575 )
576 );
577
578
579 if ( is_wp_error( $request ) || ( 200 !== wp_remote_retrieve_response_code( $request ) ) ) {
580 $this->log_failed_request();
581
582 return false;
583 }
584
585 $request = json_decode( wp_remote_retrieve_body( $request ) );
586
587 if ( $request && isset( $request->sections ) ) {
588 $request->sections = maybe_unserialize( $request->sections );
589 } else {
590 $request = false;
591 }
592
593 if ( $request && isset( $request->banners ) ) {
594 $request->banners = maybe_unserialize( $request->banners );
595 }
596
597 if ( $request && isset( $request->icons ) ) {
598 $request->icons = maybe_unserialize( $request->icons );
599 }
600
601 if ( ! empty( $request->sections ) ) {
602 foreach ( $request->sections as $key => $section ) {
603 $request->$key = (array) $section;
604 }
605 }
606
607 return $request;
608 }
609
610 /**
611 * Get the version info from the cache, if it exists.
612 *
613 * @param string $cache_key
614 *
615 * @return object
616 */
617 public function get_cached_version_info( $cache_key = '' ) {
618
619 if ( empty( $cache_key ) ) {
620 $cache_key = $this->get_cache_key();
621 }
622
623 $cache = get_option( $cache_key );
624
625 // Cache is expired
626 if ( empty( $cache['timeout'] ) || time() > $cache['timeout'] ) {
627 return false;
628 }
629
630 // We need to turn the icons into an array, thanks to WP Core forcing these into an object at some point.
631 $cache['value'] = json_decode( $cache['value'] );
632 if ( ! empty( $cache['value']->icons ) ) {
633 $cache['value']->icons = (array) $cache['value']->icons;
634 }
635
636 return $cache['value'];
637
638 }
639
640 /**
641 * Adds the plugin version information to the database.
642 *
643 * @param string $value
644 * @param string $cache_key
645 */
646 public function set_version_info_cache( $value = '', $cache_key = '' ) {
647
648 if ( empty( $cache_key ) ) {
649 $cache_key = $this->get_cache_key();
650 }
651
652 $data = array(
653 'timeout' => strtotime( '+3 hours', time() ),
654 'value' => wp_json_encode( $value ),
655 );
656
657 update_option( $cache_key, $data, 'no' );
658
659 // Delete the duplicate option
660 delete_option( 'edd_api_request_' . md5( serialize( $this->slug . $this->api_data['license'] . $this->beta ) ) );
661 }
662
663 /**
664 * Returns if the SSL of the store should be verified.
665 *
666 * @return bool
667 * @since 1.6.13
668 */
669 private function verify_ssl() {
670 return (bool) apply_filters( 'edd_sl_api_request_verify_ssl', true, $this );
671 }
672
673 /**
674 * Gets the unique key (option name) for a plugin.
675 *
676 * @return string
677 * @since 1.9.0
678 */
679 private function get_cache_key() {
680 $string = $this->slug . $this->api_data['license'] . $this->beta;
681
682 return 'edd_sl_' . md5( serialize( $string ) );
683 }
684
685 }