PluginProbe
Polylang / 3.8.6
Polylang v3.8.6
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
polylang / src / install / plugin-updater.php

plugin-updater.php in Polylang 3.8.6, at src/install/plugin-updater.php

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