PluginProbe
Polylang / 3.2.8
Polylang v3.2.8
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 / install / plugin-updater.php

plugin-updater.php in Polylang 3.2.8, at install/plugin-updater.php

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