PluginProbe
Taskbuilder – Project Management & Task Management Tool With Kanban Board / 4.0.3
Taskbuilder – Project Management & Task Management Tool With Kanban Board v4.0.3
6.0.5 6.0.2 6.0.3 6.0.4 6.0.1 6.0.0 5.0.8 5.0.9 4.0.9 5.0.0 5.0.1 5.0.2 5.0.3 5.0.4 5.0.5 5.0.6 5.0.7 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 All 57 releases
taskbuilder / includes / EDD_SL_Plugin_Updater.php

EDD_SL_Plugin_Updater.php in Taskbuilder – Project Management & Task Management Tool With Kanban Board 4.0.3, at includes/EDD_SL_Plugin_Updater.php

619 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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.9.0
13 */
14 class EDD_SL_Plugin_Updater {
15
16 private $api_url = '';
17 private $api_data = array();
18 private $plugin_file = '';
19 private $name = '';
20 private $slug = '';
21 private $version = '';
22 private $wp_override = false;
23 private $beta = false;
24 private $health_check_timeout = 5;
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;
39
40 $this->api_url = trailingslashit( $_api_url );
41 $this->api_data = $_api_data;
42 $this->plugin_file = $_plugin_file;
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
49 $edd_plugin_data[ $this->slug ] = $this->api_data;
50
51 /**
52 * Fires after the $edd_plugin_data is setup.
53 *
54 * @since x.x.x
55 *
56 * @param array $edd_plugin_data Array of EDD SL plugin data.
57 */
58 do_action( 'post_edd_sl_plugin_updater_setup', $edd_plugin_data );
59
60 // Set up hooks.
61 $this->init();
62
63 }
64
65 /**
66 * Set up WordPress filters to hook into WP's update process.
67 *
68 * @uses add_filter()
69 *
70 * @return void
71 */
72 public function init() {
73
74 add_filter( 'pre_set_site_transient_update_plugins', array( $this, 'check_update' ) );
75 add_filter( 'plugins_api', array( $this, 'plugins_api_filter' ), 10, 3 );
76 add_action( 'after_plugin_row', array( $this, 'show_update_notification' ), 10, 2 );
77 add_action( 'admin_init', array( $this, 'show_changelog' ) );
78
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 * @uses api_request()
90 *
91 * @param array $_transient_data Update array build by WordPress.
92 * @return array Modified update array with custom plugin data.
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 ( ! empty( $_transient_data->response ) && ! empty( $_transient_data->response[ $this->name ] ) && false === $this->wp_override ) {
103 return $_transient_data;
104 }
105
106 $current = $this->get_repo_api_data();
107 if ( false !== $current && is_object( $current ) && isset( $current->new_version ) ) {
108 if ( version_compare( $this->version, $current->new_version, '<' ) ) {
109 $_transient_data->response[ $this->name ] = $current;
110 } else {
111 // Populating the no_update information is required to support auto-updates in WordPress 5.5.
112 $_transient_data->no_update[ $this->name ] = $current;
113 }
114 }
115 $_transient_data->last_checked = time();
116 $_transient_data->checked[ $this->name ] = $this->version;
117
118 return $_transient_data;
119 }
120
121 /**
122 * Get repo API data from store.
123 * Save to cache.
124 *
125 * @return \stdClass
126 */
127 public function get_repo_api_data() {
128 $version_info = $this->get_cached_version_info();
129
130 if ( false === $version_info ) {
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
146 $this->set_version_info_cache( $version_info );
147 }
148
149 return $version_info;
150 }
151
152 /**
153 * Show the update notification on multisite subsites.
154 *
155 * @param string $file
156 * @param array $plugin
157 */
158 public function show_update_notification( $file, $plugin ) {
159
160 // Return early if in the network admin, or if this is not a multisite install.
161 if ( is_network_admin() || ! is_multisite() ) {
162 return;
163 }
164
165 if ( ! current_user_can( 'update_plugins' ) ) {
166 return;
167 }
168
169 if ( $this->name !== $file ) {
170 return;
171 }
172
173 // Do not print any message if update does not exist.
174 $update_cache = get_site_transient( 'update_plugins' );
175
176 if ( ! isset( $update_cache->response[ $this->name ] ) ) {
177 if ( ! is_object( $update_cache ) ) {
178 $update_cache = new stdClass();
179 }
180 $update_cache->response[ $this->name ] = $this->get_repo_api_data();
181 }
182
183 // 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.
184 if ( empty( $update_cache->response[ $this->name ] ) || version_compare( $this->version, $update_cache->response[ $this->name ]->new_version, '>=' ) ) {
185 return;
186 }
187
188 printf(
189 '<tr class="plugin-update-tr %3$s" id="%1$s-update" data-slug="%1$s" data-plugin="%2$s">',
190 $this->slug,
191 $file,
192 in_array( $this->name, $this->get_active_plugins(), true ) ? 'active' : 'inactive'
193 );
194
195 echo '<td colspan="3" class="plugin-update colspanchange">';
196 echo '<div class="update-message notice inline notice-warning notice-alt"><p>';
197
198 $changelog_link = '';
199 if ( ! empty( $update_cache->response[ $this->name ]->sections->changelog ) ) {
200 $changelog_link = add_query_arg(
201 array(
202 'edd_sl_action' => 'view_plugin_changelog',
203 'plugin' => urlencode( $this->name ),
204 'slug' => urlencode( $this->slug ),
205 'TB_iframe' => 'true',
206 'width' => 77,
207 'height' => 911,
208 ),
209 self_admin_url( 'index.php' )
210 );
211 }
212 $update_link = add_query_arg(
213 array(
214 'action' => 'upgrade-plugin',
215 'plugin' => urlencode( $this->name ),
216 ),
217 self_admin_url( 'update.php' )
218 );
219
220 printf(
221 /* translators: the plugin name. */
222 esc_html__( 'There is a new version of %1$s available.', 'easy-digital-downloads' ),
223 esc_html( $plugin['Name'] )
224 );
225
226 if ( empty( $update_cache->response[ $this->name ]->package ) && ! empty( $changelog_link ) ) {
227 printf(
228 /* translators: 1. opening anchor tag, do not translate 2. the new plugin version 3. closing anchor tag, do not translate. */
229 __( ' %1$sView version %2$s details%3$s.', 'easy-digital-downloads' ),
230 '<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">',
231 esc_html( $update_cache->response[ $this->name ]->new_version ),
232 '</a>'
233 );
234 } elseif ( ! empty( $changelog_link ) ) {
235 printf(
236 __( ' %1$sView version %2$s details%3$s or %4$supdate now%5$s.', 'easy-digital-downloads' ),
237 '<a target="_blank" class="thickbox open-plugin-details-modal" href="' . esc_url( $changelog_link ) . '">',
238 esc_html( $update_cache->response[ $this->name ]->new_version ),
239 '</a>',
240 '<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">',
241 '</a>'
242 );
243 } else {
244 printf(
245 ' %1$s%2$s%3$s',
246 '<a target="_blank" class="update-link" href="' . esc_url( wp_nonce_url( $update_link, 'upgrade-plugin_' . $file ) ) . '">',
247 esc_html__( 'Update now.', 'easy-digital-downloads' ),
248 '</a>'
249 );
250 }
251
252 do_action( "in_plugin_update_message-{$file}", $plugin, $plugin );
253
254 echo '</p></div></td></tr>';
255 }
256
257 /**
258 * Gets the plugins active in a multisite network.
259 *
260 * @return array
261 */
262 private function get_active_plugins() {
263 $active_plugins = (array) get_option( 'active_plugins' );
264 $active_network_plugins = (array) get_site_option( 'active_sitewide_plugins' );
265
266 return array_merge( $active_plugins, array_keys( $active_network_plugins ) );
267 }
268
269 /**
270 * Updates information on the "View version x.x details" page with custom data.
271 *
272 * @uses api_request()
273 *
274 * @param mixed $_data
275 * @param string $_action
276 * @param object $_args
277 * @return object $_data
278 */
279 public function plugins_api_filter( $_data, $_action = '', $_args = null ) {
280
281 if ( 'plugin_information' !== $_action ) {
282
283 return $_data;
284
285 }
286
287 if ( ! isset( $_args->slug ) || ( $_args->slug !== $this->slug ) ) {
288
289 return $_data;
290
291 }
292
293 $to_send = array(
294 'slug' => $this->slug,
295 'is_ssl' => is_ssl(),
296 'fields' => array(
297 'banners' => array(),
298 'reviews' => false,
299 'icons' => array(),
300 ),
301 );
302
303 // Get the transient where we store the api request for this plugin for 24 hours
304 $edd_api_request_transient = $this->get_cached_version_info();
305
306 //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.
307 if ( empty( $edd_api_request_transient ) ) {
308
309 $api_response = $this->api_request( 'plugin_information', $to_send );
310
311 // Expires in 3 hours
312 $this->set_version_info_cache( $api_response );
313
314 if ( false !== $api_response ) {
315 $_data = $api_response;
316 }
317 } else {
318 $_data = $edd_api_request_transient;
319 }
320
321 // Convert sections into an associative array, since we're getting an object, but Core expects an array.
322 if ( isset( $_data->sections ) && ! is_array( $_data->sections ) ) {
323 $_data->sections = $this->convert_object_to_array( $_data->sections );
324 }
325
326 // Convert banners into an associative array, since we're getting an object, but Core expects an array.
327 if ( isset( $_data->banners ) && ! is_array( $_data->banners ) ) {
328 $_data->banners = $this->convert_object_to_array( $_data->banners );
329 }
330
331 // Convert icons into an associative array, since we're getting an object, but Core expects an array.
332 if ( isset( $_data->icons ) && ! is_array( $_data->icons ) ) {
333 $_data->icons = $this->convert_object_to_array( $_data->icons );
334 }
335
336 // Convert contributors into an associative array, since we're getting an object, but Core expects an array.
337 if ( isset( $_data->contributors ) && ! is_array( $_data->contributors ) ) {
338 $_data->contributors = $this->convert_object_to_array( $_data->contributors );
339 }
340
341 if ( ! isset( $_data->plugin ) ) {
342 $_data->plugin = $this->name;
343 }
344
345 return $_data;
346 }
347
348 /**
349 * Convert some objects to arrays when injecting data into the update API
350 *
351 * Some data like sections, banners, and icons are expected to be an associative array, however due to the JSON
352 * decoding, they are objects. This method allows us to pass in the object and return an associative array.
353 *
354 * @since 3.6.5
355 *
356 * @param stdClass $data
357 *
358 * @return array
359 */
360 private function convert_object_to_array( $data ) {
361 if ( ! is_array( $data ) && ! is_object( $data ) ) {
362 return array();
363 }
364 $new_data = array();
365 foreach ( $data as $key => $value ) {
366 $new_data[ $key ] = is_object( $value ) ? $this->convert_object_to_array( $value ) : $value;
367 }
368
369 return $new_data;
370 }
371
372 /**
373 * Disable SSL verification in order to prevent download update failures
374 *
375 * @param array $args
376 * @param string $url
377 * @return object $array
378 */
379 public function http_request_args( $args, $url ) {
380
381 if ( strpos( $url, 'https://' ) !== false && strpos( $url, 'edd_action=package_download' ) ) {
382 $args['sslverify'] = $this->verify_ssl();
383 }
384 return $args;
385
386 }
387
388 /**
389 * Calls the API and, if successfull, returns the object delivered by the API.
390 *
391 * @uses get_bloginfo()
392 * @uses wp_remote_post()
393 * @uses is_wp_error()
394 *
395 * @param string $_action The requested action.
396 * @param array $_data Parameters for the API action.
397 * @return false|object
398 */
399 private function api_request( $_action, $_data ) {
400
401 global $edd_plugin_url_available;
402
403 // Do a quick status check on this domain if we haven't already checked it.
404 $store_hash = md5( $this->api_url );
405 if ( ! is_array( $edd_plugin_url_available ) || ! isset( $edd_plugin_url_available[ $store_hash ] ) ) {
406 $test_url_parts = wp_parse_url( $this->api_url );
407
408 $scheme = ! empty( $test_url_parts['scheme'] ) ? $test_url_parts['scheme'] : 'http';
409 $host = ! empty( $test_url_parts['host'] ) ? $test_url_parts['host'] : '';
410 $port = ! empty( $test_url_parts['port'] ) ? ':' . $test_url_parts['port'] : '';
411
412 if ( empty( $host ) ) {
413 $edd_plugin_url_available[ $store_hash ] = false;
414 } else {
415 $test_url = $scheme . '://' . $host . $port;
416 $response = wp_remote_get(
417 $test_url,
418 array(
419 'timeout' => $this->health_check_timeout,
420 'sslverify' => $this->verify_ssl(),
421 )
422 );
423 $edd_plugin_url_available[ $store_hash ] = is_wp_error( $response ) ? false : true;
424 }
425 }
426
427 if ( false === $edd_plugin_url_available[ $store_hash ] ) {
428 return false;
429 }
430
431 $data = array_merge( $this->api_data, $_data );
432
433 if ( $data['slug'] !== $this->slug ) {
434 return;
435 }
436
437 // Don't allow a plugin to ping itself
438 if ( trailingslashit( home_url() ) === $this->api_url ) {
439 return false;
440 }
441
442 return $this->get_version_from_remote();
443 }
444
445 /**
446 * If available, show the changelog for sites in a multisite install.
447 */
448 public function show_changelog() {
449
450 if ( empty( $_REQUEST['edd_sl_action'] ) || 'view_plugin_changelog' !== $_REQUEST['edd_sl_action'] ) {
451 return;
452 }
453
454 if ( empty( $_REQUEST['plugin'] ) ) {
455 return;
456 }
457
458 if ( empty( $_REQUEST['slug'] ) || $this->slug !== $_REQUEST['slug'] ) {
459 return;
460 }
461
462 if ( ! current_user_can( 'update_plugins' ) ) {
463 wp_die( esc_html__( 'You do not have permission to install plugin updates', 'easy-digital-downloads' ), esc_html__( 'Error', 'easy-digital-downloads' ), array( 'response' => 403 ) );
464 }
465
466 $version_info = $this->get_repo_api_data();
467 if ( isset( $version_info->sections ) ) {
468 $sections = $this->convert_object_to_array( $version_info->sections );
469 if ( ! empty( $sections['changelog'] ) ) {
470 echo '<div style="background:#fff;padding:10px;">' . wp_kses_post( $sections['changelog'] ) . '</div>';
471 }
472 }
473
474 exit;
475 }
476
477 /**
478 * Gets the current version information from the remote site.
479 *
480 * @return array|false
481 */
482 private function get_version_from_remote() {
483 $api_params = array(
484 'edd_action' => 'get_version',
485 'license' => ! empty( $this->api_data['license'] ) ? $this->api_data['license'] : '',
486 'item_name' => isset( $this->api_data['item_name'] ) ? $this->api_data['item_name'] : false,
487 'item_id' => isset( $this->api_data['item_id'] ) ? $this->api_data['item_id'] : false,
488 'version' => isset( $this->api_data['version'] ) ? $this->api_data['version'] : false,
489 'slug' => $this->slug,
490 'author' => $this->api_data['author'],
491 'url' => home_url(),
492 'beta' => $this->beta,
493 'php_version' => phpversion(),
494 'wp_version' => get_bloginfo( 'version' ),
495 );
496
497 /**
498 * Filters the parameters sent in the API request.
499 *
500 * @param array $api_params The array of data sent in the request.
501 * @param array $this->api_data The array of data set up in the class constructor.
502 * @param string $this->plugin_file The full path and filename of the file.
503 */
504 $api_params = apply_filters( 'edd_sl_plugin_updater_api_params', $api_params, $this->api_data, $this->plugin_file );
505
506 $request = wp_remote_post(
507 $this->api_url,
508 array(
509 'timeout' => 15,
510 'sslverify' => $this->verify_ssl(),
511 'body' => $api_params,
512 )
513 );
514
515 if ( is_wp_error( $request ) ) {
516 return false;
517 }
518
519 $request = json_decode( wp_remote_retrieve_body( $request ) );
520
521 if ( $request && isset( $request->sections ) ) {
522 $request->sections = maybe_unserialize( $request->sections );
523 } else {
524 $request = false;
525 }
526
527 if ( $request && isset( $request->banners ) ) {
528 $request->banners = maybe_unserialize( $request->banners );
529 }
530
531 if ( $request && isset( $request->icons ) ) {
532 $request->icons = maybe_unserialize( $request->icons );
533 }
534
535 if ( ! empty( $request->sections ) ) {
536 foreach ( $request->sections as $key => $section ) {
537 $request->$key = (array) $section;
538 }
539 }
540
541 return $request;
542 }
543
544 /**
545 * Get the version info from the cache, if it exists.
546 *
547 * @param string $cache_key
548 * @return object
549 */
550 public function get_cached_version_info( $cache_key = '' ) {
551
552 if ( empty( $cache_key ) ) {
553 $cache_key = $this->get_cache_key();
554 }
555
556 $cache = get_option( $cache_key );
557
558 // Cache is expired
559 if ( empty( $cache['timeout'] ) || time() > $cache['timeout'] ) {
560 return false;
561 }
562
563 // We need to turn the icons into an array, thanks to WP Core forcing these into an object at some point.
564 $cache['value'] = json_decode( $cache['value'] );
565 if ( ! empty( $cache['value']->icons ) ) {
566 $cache['value']->icons = (array) $cache['value']->icons;
567 }
568
569 return $cache['value'];
570
571 }
572
573 /**
574 * Adds the plugin version information to the database.
575 *
576 * @param string $value
577 * @param string $cache_key
578 */
579 public function set_version_info_cache( $value = '', $cache_key = '' ) {
580
581 if ( empty( $cache_key ) ) {
582 $cache_key = $this->get_cache_key();
583 }
584
585 $data = array(
586 'timeout' => strtotime( '+3 hours', time() ),
587 'value' => wp_json_encode( $value ),
588 );
589
590 update_option( $cache_key, $data, 'no' );
591
592 // Delete the duplicate option
593 delete_option( 'edd_api_request_' . md5( serialize( $this->slug . $this->api_data['license'] . $this->beta ) ) );
594 }
595
596 /**
597 * Returns if the SSL of the store should be verified.
598 *
599 * @since 1.6.13
600 * @return bool
601 */
602 private function verify_ssl() {
603 return (bool) apply_filters( 'edd_sl_api_request_verify_ssl', true, $this );
604 }
605
606 /**
607 * Gets the unique key (option name) for a plugin.
608 *
609 * @since 1.9.0
610 * @return string
611 */
612 private function get_cache_key() {
613 $string = $this->slug . $this->api_data['license'] . $this->beta;
614
615 return 'edd_sl_' . md5( serialize( $string ) );
616 }
617
618 }
619