PluginProbe
StreamCast – bring live radio to your site with a sleek player / trunk
StreamCast – bring live radio to your site with a sleek player vtrunk
2.4.5 2.4.4 trunk 1.0 1.1 2.0.0 2.1.10 2.1.2 2.1.4 2.1.5 2.1.8 2.2.1 2.2.3 2.2.4 2.2.5 2.3.0 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.3.6 2.3.7 2.3.8 2.3.9 All 29 releases
streamcast / vendor / freemius / includes / class-fs-plugin-updater.php

class-fs-plugin-updater.php in StreamCast – bring live radio to your site with a sleek player trunk, at vendor/freemius/includes/class-fs-plugin-updater.php

1,628 lines 65.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * @package Freemius
4 * @copyright Copyright (c) 2015, Freemius, Inc.
5 * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
6 * @since 1.0.4
7 *
8 * @link https://github.com/easydigitaldownloads/EDD-License-handler/blob/master/EDD_SL_Plugin_Updater.php
9 */
10
11 if ( ! defined( 'ABSPATH' ) ) {
12 exit;
13 }
14
15 class FS_Plugin_Updater {
16
17 /**
18 * @var Freemius
19 * @since 1.0.4
20 */
21 private $_fs;
22 /**
23 * @var FS_Logger
24 * @since 1.0.4
25 */
26 private $_logger;
27 /**
28 * @var object
29 * @since 1.1.8.1
30 */
31 private $_update_details;
32 /**
33 * @var array
34 * @since 2.1.2
35 */
36 private $_translation_updates;
37
38 private static $_upgrade_basename = null;
39
40 const UPDATES_CHECK_CACHE_EXPIRATION = ( WP_FS__TIME_24_HOURS_IN_SEC / 24 );
41
42 #--------------------------------------------------------------------------------
43 #region Singleton
44 #--------------------------------------------------------------------------------
45
46 /**
47 * @var FS_Plugin_Updater[]
48 * @since 2.0.0
49 */
50 private static $_INSTANCES = array();
51
52 /**
53 * @param Freemius $freemius
54 *
55 * @return FS_Plugin_Updater
56 */
57 static function instance( Freemius $freemius ) {
58 $key = $freemius->get_id();
59
60 if ( ! isset( self::$_INSTANCES[ $key ] ) ) {
61 self::$_INSTANCES[ $key ] = new self( $freemius );
62 }
63
64 return self::$_INSTANCES[ $key ];
65 }
66
67 #endregion
68
69 private function __construct( Freemius $freemius ) {
70 $this->_fs = $freemius;
71
72 $this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $freemius->get_slug() . '_updater', WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
73
74 $this->filters();
75 }
76
77 /**
78 * Initiate required filters.
79 *
80 * @author Vova Feldman (@svovaf)
81 * @since 1.0.4
82 */
83 private function filters() {
84 // Override request for plugin information
85 add_filter( 'plugins_api', array( &$this, 'plugins_api_filter' ), 10, 3 );
86
87 $this->add_transient_filters();
88
89 /**
90 * If user has the premium plugin's code but do NOT have an active license,
91 * encourage him to upgrade by showing that there's a new release, but instead
92 * of showing an update link, show upgrade link to the pricing page.
93 *
94 * @since 1.1.6
95 *
96 */
97 // WP 2.9+
98 add_action( "after_plugin_row_{$this->_fs->get_plugin_basename()}", array(
99 &$this,
100 'catch_plugin_update_row'
101 ), 9 );
102 add_action( "after_plugin_row_{$this->_fs->get_plugin_basename()}", array(
103 &$this,
104 'edit_and_echo_plugin_update_row'
105 ), 11, 2 );
106
107 if ( ! $this->_fs->has_any_active_valid_license() ) {
108 add_action( 'admin_head', array( &$this, 'catch_plugin_information_dialog_contents' ) );
109 } else {
110 add_action( 'admin_footer', array( &$this, '_add_fs_allow_updater_and_dialog_request_param' ) );
111 }
112
113 if ( ! WP_FS__IS_PRODUCTION_MODE ) {
114 add_filter( 'http_request_host_is_external', array(
115 $this,
116 'http_request_host_is_external_filter'
117 ), 10, 3 );
118 }
119
120 if ( $this->_fs->is_premium() ) {
121 if ( ! $this->is_correct_folder_name() ) {
122 add_filter( 'upgrader_post_install', array( &$this, '_maybe_update_folder_name' ), 10, 3 );
123 }
124
125 add_filter( 'upgrader_pre_install', array( 'FS_Plugin_Updater', '_store_basename_for_source_adjustment' ), 1, 2 );
126 add_filter( 'upgrader_source_selection', array( 'FS_Plugin_Updater', '_maybe_adjust_source_dir' ), 1, 3 );
127
128 if ( ! $this->_fs->has_any_active_valid_license() ) {
129 add_filter( 'wp_prepare_themes_for_js', array( &$this, 'change_theme_update_info_html' ), 10, 1 );
130 }
131 }
132 }
133
134 /**
135 * @author Leo Fajardo (@leorw)
136 * @since 2.7.4
137 */
138 function _add_fs_allow_updater_and_dialog_request_param() {
139 if ( ! $this->is_plugin_information_dialog_for_plugin() ) {
140 return;
141 }
142 ?>
143 <script type="text/javascript">
144 if ( typeof jQuery !== 'undefined' ) {
145 jQuery( document ).on( 'wp-plugin-updating', function( event, args ) {
146 if ( typeof args === 'object' && args.slug && typeof args.slug === 'string' ) {
147 if ( <?php echo json_encode( $this->_fs->get_slug() ) ?> === args.slug ) {
148 args.fs_allow_updater_and_dialog = true;
149 }
150 }
151 } );
152 }
153 </script>
154 <?php
155 }
156
157 /**
158 * @author Leo Fajardo (@leorw)
159 * @since 2.7.4
160 *
161 * @return bool
162 */
163 private function is_plugin_information_dialog_for_plugin() {
164 return (
165 'plugin-information' === fs_request_get( 'tab', false ) &&
166 $this->_fs->get_slug() === fs_request_get_raw( 'plugin', false )
167 );
168 }
169
170 /**
171 * @author Leo Fajardo (@leorw)
172 * @since 2.1.4
173 */
174 function catch_plugin_information_dialog_contents() {
175 if ( ! $this->is_plugin_information_dialog_for_plugin() ) {
176 return;
177 }
178
179 add_action( 'admin_footer', array( &$this, 'edit_and_echo_plugin_information_dialog_contents' ), 0, 1 );
180
181 ob_start();
182 }
183
184 /**
185 * @author Leo Fajardo (@leorw)
186 * @since 2.1.4
187 *
188 * @param string $hook_suffix
189 */
190 function edit_and_echo_plugin_information_dialog_contents( $hook_suffix ) {
191 if (
192 'plugin-information' !== fs_request_get( 'tab', false ) ||
193 $this->_fs->get_slug() !== fs_request_get_raw( 'plugin', false )
194 ) {
195 return;
196 }
197
198 $license = $this->_fs->_get_license();
199
200 $subscription = ( is_object( $license ) && ! $license->is_lifetime() ) ?
201 $this->_fs->_get_subscription( $license->id ) :
202 null;
203
204 $contents = ob_get_clean();
205
206 $install_or_update_button_id_attribute_pos = strpos( $contents, 'id="plugin_install_from_iframe"' );
207
208 if ( false === $install_or_update_button_id_attribute_pos ) {
209 $install_or_update_button_id_attribute_pos = strpos( $contents, 'id="plugin_update_from_iframe"' );
210 }
211
212 if ( false !== $install_or_update_button_id_attribute_pos ) {
213 $install_or_update_button_start_pos = strrpos(
214 substr( $contents, 0, $install_or_update_button_id_attribute_pos ),
215 '<a'
216 );
217
218 $install_or_update_button_end_pos = ( strpos( $contents, '</a>', $install_or_update_button_id_attribute_pos ) + strlen( '</a>' ) );
219
220 /**
221 * The part of the contents without the update button.
222 *
223 * @author Leo Fajardo (@leorw)
224 * @since 2.2.5
225 */
226 $modified_contents = substr( $contents, 0, $install_or_update_button_start_pos );
227
228 $install_or_update_button = substr( $contents, $install_or_update_button_start_pos, ( $install_or_update_button_end_pos - $install_or_update_button_start_pos ) );
229
230 /**
231 * Replace the plugin information dialog's "Install Update Now" button's text and URL. If there's a license,
232 * the text will be "Renew license" and will link to the checkout page with the license's billing cycle
233 * and quota. If there's no license, the text will be "Buy license" and will link to the pricing page.
234 */
235 $install_or_update_button = preg_replace(
236 '/(\<a.+)(id="plugin_(install|update)_from_iframe")(.+href=")([^\s]+)(".*\>)(.+)(\<\/a>)/is',
237 is_object( $license ) ?
238 sprintf(
239 '$1$4%s$6%s$8',
240 $this->_fs->checkout_url(
241 is_object( $subscription ) ?
242 ( 1 == $subscription->billing_cycle ? WP_FS__PERIOD_MONTHLY : WP_FS__PERIOD_ANNUALLY ) :
243 WP_FS__PERIOD_LIFETIME,
244 false,
245 array( 'licenses' => $license->quota )
246 ),
247 fs_text_inline( 'Renew license', 'renew-license', $this->_fs->get_slug() )
248 ) :
249 sprintf(
250 '$1$4%s$6%s$8',
251 $this->_fs->pricing_url(),
252 fs_text_inline( 'Buy license', 'buy-license', $this->_fs->get_slug() )
253 ),
254 $install_or_update_button
255 );
256
257 /**
258 * Append the modified button.
259 *
260 * @author Leo Fajardo (@leorw)
261 * @since 2.2.5
262 */
263 $modified_contents .= $install_or_update_button;
264
265 /**
266 * Append the remaining part of the contents after the update button.
267 *
268 * @author Leo Fajardo (@leorw)
269 * @since 2.2.5
270 */
271 $modified_contents .= substr( $contents, $install_or_update_button_end_pos );
272
273 $contents = $modified_contents;
274 }
275
276 echo $contents;
277 }
278
279 /**
280 * @author Vova Feldman (@svovaf)
281 * @since 2.0.0
282 */
283 private function add_transient_filters() {
284 if (
285 $this->_fs->is_premium() &&
286 $this->_fs->is_registered() &&
287 ! FS_Permission_Manager::instance( $this->_fs )->is_essentials_tracking_allowed()
288 ) {
289 $this->_logger->log( 'Opted out sites cannot receive automatic software updates.' );
290
291 return;
292 }
293
294 add_filter( 'pre_set_site_transient_update_plugins', array(
295 &$this,
296 'pre_set_site_transient_update_plugins_filter'
297 ) );
298
299 add_filter( 'pre_set_site_transient_update_themes', array(
300 &$this,
301 'pre_set_site_transient_update_plugins_filter'
302 ) );
303 }
304
305 /**
306 * @author Vova Feldman (@svovaf)
307 * @since 2.0.0
308 */
309 private function remove_transient_filters() {
310 remove_filter( 'pre_set_site_transient_update_plugins', array(
311 &$this,
312 'pre_set_site_transient_update_plugins_filter'
313 ) );
314
315 remove_filter( 'pre_set_site_transient_update_themes', array(
316 &$this,
317 'pre_set_site_transient_update_plugins_filter'
318 ) );
319 }
320
321 /**
322 * Capture plugin update row by turning output buffering.
323 *
324 * @author Vova Feldman (@svovaf)
325 * @since 1.1.6
326 */
327 function catch_plugin_update_row() {
328 ob_start();
329 }
330
331 /**
332 * Overrides default update message format with "renew your license" message.
333 *
334 * @author Vova Feldman (@svovaf)
335 * @since 1.1.6
336 *
337 * @param string $file
338 * @param array $plugin_data
339 */
340 function edit_and_echo_plugin_update_row( $file, $plugin_data ) {
341 $plugin_update_row = ob_get_clean();
342
343 $current = get_site_transient( 'update_plugins' );
344 if ( ! isset( $current->response[ $file ] ) ) {
345 echo $plugin_update_row;
346
347 return;
348 }
349
350 $r = $current->response[ $file ];
351
352 $has_beta_update = $this->_fs->has_beta_update();
353
354 if ( $this->_fs->has_any_active_valid_license() ) {
355 if ( $has_beta_update ) {
356 /**
357 * Turn the "new version" text into "new Beta version".
358 *
359 * Sample input:
360 * There is a new version of Awesome Plugin available. <a href="...>View version x.y.z details</a> or <a href="...>update now</a>.
361 * Output:
362 * There is a new Beta version of Awesome Plugin available. <a href="...>View version x.y.z details</a> or <a href="...>update now</a>.
363 *
364 * @author Leo Fajardo (@leorw)
365 * @since 2.3.0
366 */
367 $plugin_update_row = preg_replace(
368 '/(\<div.+>)(.+)(\<a.+href="([^\s]+)"([^\<]+)\>.+\<a.+)(\<\/div\>)/is',
369 (
370 '$1' .
371 sprintf(
372 fs_text_inline( 'There is a %s of %s available.', 'new-version-available', $this->_fs->get_slug() ),
373 $has_beta_update ?
374 fs_text_inline( 'new Beta version', 'new-beta-version', $this->_fs->get_slug() ) :
375 fs_text_inline( 'new version', 'new-version', $this->_fs->get_slug() ),
376 $this->_fs->get_plugin_title()
377 ) .
378 ' ' .
379 '$3' .
380 '$6'
381 ),
382 $plugin_update_row
383 );
384 }
385 } else {
386 /**
387 * Turn the "new version" text into a link that opens the plugin information dialog when clicked and
388 * make the "View version x details" text link to the checkout page instead of opening the plugin
389 * information dialog when clicked.
390 *
391 * Sample input:
392 * There is a new version of Awesome Plugin available. <a href="...>View version x.y.z details</a> or <a href="...>update now</a>.
393 * Output:
394 * There is a <a href="...>new version</a> of Awesome Plugin available. <a href="...>Buy a license now</a> to access version x.y.z security & feature updates, and support.
395 * OR
396 * There is a <a href="...>new Beta version</a> of Awesome Plugin available. <a href="...>Buy a license now</a> to access version x.y.z security & feature updates, and support.
397 *
398 * @author Leo Fajardo (@leorw)
399 */
400 $plugin_update_row = preg_replace(
401 '/(\<div.+>)(.+)(\<a.+href="([^\s]+)"([^\<]+)\>.+\<a.+)(\<\/div\>)/is',
402 (
403 '$1' .
404 sprintf(
405 fs_text_inline( 'There is a %s of %s available.', 'new-version-available', $this->_fs->get_slug() ),
406 sprintf(
407 '<a href="$4"%s>%s</a>',
408 '$5',
409 $has_beta_update ?
410 fs_text_inline( 'new Beta version', 'new-beta-version', $this->_fs->get_slug() ) :
411 fs_text_inline( 'new version', 'new-version', $this->_fs->get_slug() )
412 ),
413 $this->_fs->get_plugin_title()
414 ) .
415 ' ' .
416 $this->_fs->version_upgrade_checkout_link( $r->new_version ) .
417 '$6'
418 ),
419 $plugin_update_row
420 );
421 }
422
423 if (
424 $this->_fs->is_plugin() &&
425 isset( $r->upgrade_notice ) &&
426 strlen( trim( $r->upgrade_notice ) ) > 0
427 ) {
428 $slug = $this->_fs->get_slug();
429
430 $upgrade_notice_html = sprintf(
431 '<p class="notice fs-upgrade-notice fs-slug-%1$s fs-type-%2$s" data-slug="%1$s" data-type="%2$s"><strong>%3$s</strong> %4$s</p>',
432 $slug,
433 $this->_fs->get_module_type(),
434 fs_text_inline( 'Important Upgrade Notice:', 'upgrade_notice', $slug ),
435 esc_html( $r->upgrade_notice )
436 );
437
438 $plugin_update_row = str_replace( '</div>', '</div>' . $upgrade_notice_html, $plugin_update_row );
439 }
440
441 echo $plugin_update_row;
442 }
443
444 /**
445 * @author Leo Fajardo (@leorw)
446 * @since 2.0.2
447 *
448 * @param array $prepared_themes
449 *
450 * @return array
451 */
452 function change_theme_update_info_html( $prepared_themes ) {
453 $theme_basename = $this->_fs->get_plugin_basename();
454
455 if ( ! isset( $prepared_themes[ $theme_basename ] ) ) {
456 return $prepared_themes;
457 }
458
459 $themes_update = get_site_transient( 'update_themes' );
460 if ( ! isset( $themes_update->response[ $theme_basename ] ) ||
461 empty( $themes_update->response[ $theme_basename ]['package'] )
462 ) {
463 return $prepared_themes;
464 }
465
466 $prepared_themes[ $theme_basename ]['update'] = preg_replace(
467 '/(\<p.+>)(.+)(\<a.+\<a.+)\.(.+\<\/p\>)/is',
468 '$1 $2 ' . $this->_fs->version_upgrade_checkout_link( $themes_update->response[ $theme_basename ]['new_version'] ) .
469 '$4',
470 $prepared_themes[ $theme_basename ]['update']
471 );
472
473 // Set to false to prevent the "Update now" link for the context theme from being shown on the "Themes" page.
474 $prepared_themes[ $theme_basename ]['hasPackage'] = false;
475
476 return $prepared_themes;
477 }
478
479 /**
480 * Since WP version 3.6, a new security feature was added that denies access to repository with a local ip.
481 * During development mode we want to be able updating plugin versions via our localhost repository. This
482 * filter white-list all domains including "api.freemius".
483 *
484 * @link http://www.emanueletessore.com/wordpress-download-failed-valid-url-provided/
485 *
486 * @author Vova Feldman (@svovaf)
487 * @since 1.0.4
488 *
489 * @param bool $allow
490 * @param string $host
491 * @param string $url
492 *
493 * @return bool
494 */
495 function http_request_host_is_external_filter( $allow, $host, $url ) {
496 return ( false !== strpos( $host, 'freemius' ) ) ? true : $allow;
497 }
498
499 /**
500 * Check for Updates at the defined API endpoint and modify the update array.
501 *
502 * This function dives into the update api just when WordPress creates its update array,
503 * then adds a custom API call and injects the custom plugin data retrieved from the API.
504 * It is reassembled from parts of the native WordPress plugin update code.
505 * See wp-includes/update.php line 121 for the original wp_update_plugins() function.
506 *
507 * @author Vova Feldman (@svovaf)
508 * @since 1.0.4
509 *
510 * @uses FS_Api
511 *
512 * @param object $transient_data Update array build by WordPress.
513 *
514 * @return object Modified update array with custom plugin data.
515 */
516 function pre_set_site_transient_update_plugins_filter( $transient_data ) {
517 $this->_logger->entrance();
518
519 /**
520 * "plugins" or "themes".
521 *
522 * @author Leo Fajardo (@leorw)
523 * @since 1.2.2
524 */
525 $module_type = $this->_fs->get_module_type() . 's';
526
527 /**
528 * Ensure that we don't mix plugins update info with themes update info.
529 *
530 * @author Leo Fajardo (@leorw)
531 * @since 1.2.2
532 */
533 if ( "pre_set_site_transient_update_{$module_type}" !== current_filter() ) {
534 return $transient_data;
535 }
536
537 if ( empty( $transient_data ) ||
538 defined( 'WP_FS__UNINSTALL_MODE' )
539 ) {
540 return $transient_data;
541 }
542
543 // Alias.
544 $basename = $this->_fs->premium_plugin_basename();
545
546 global $wp_current_filter;
547
548 /**
549 * During bulk updates, avoid re-injecting update data for the plugin itself once it has already been updated.
550 *
551 * If the custom package is re-added to the transient after the plugin update, WordPress may detect the package again and incorrectly report "The plugin is at the latest version" for a pending update, since the custom package version matches the currently installed version.
552 *
553 * Behavior differs depending on how the bulk update is triggered. Please refer to the inline comments for each flow below for details.
554 */
555 if (
556 ! empty( $wp_current_filter ) && (
557 /**
558 * update-core.php and other upgrader pages:
559 * The `upgrader_process_complete` action fires only once after all updates have finished. In this case, it is the current action (`$wp_current_filter[0]`), while `self::$_upgrade_basename` may contain any plugin basename.
560 */
561 'upgrader_process_complete' === $wp_current_filter[0] ||
562 /**
563 * AJAX bulk updates (e.g., from the Plugins page):
564 * The `upgrader_process_complete` action fires multiple times — once for each plugin after it finishes updating. In this flow, it is not the current action (`$wp_current_filter[0]`) because it is triggered from another action. Instead, we compare `self::$_upgrade_basename` with the basename of the plugin currently being updated, since the `upgrader_process_complete` action runs separately for each plugin.
565 */
566 ( in_array( 'upgrader_process_complete', $wp_current_filter ) && self::$_upgrade_basename === $basename )
567 )
568 ) {
569 return $transient_data;
570 }
571
572 if ( ! isset( $this->_update_details ) ) {
573 // Get plugin's newest update.
574 $new_version = $this->_fs->get_update( false, fs_request_get_bool( 'force-check' ) );
575
576 $this->_update_details = false;
577
578 if ( is_object( $new_version ) && $this->is_new_version_premium( $new_version ) ) {
579 $this->_logger->log( 'Found newer plugin version ' . $new_version->version );
580
581 /**
582 * Cache plugin details locally since set_site_transient( 'update_plugins' )
583 * called multiple times and the non wp.org plugins are filtered after the
584 * call to .org.
585 *
586 * @since 1.1.8.1
587 */
588 $this->_update_details = $this->get_update_details( $new_version );
589 }
590 }
591
592 if ( is_object( $this->_update_details ) ) {
593 if ( isset( $transient_data->no_update ) ) {
594 unset( $transient_data->no_update[ $basename ] );
595 }
596
597 if ( ! isset( $transient_data->response ) ) {
598 $transient_data->response = array();
599 }
600
601 // Add plugin to transient data.
602 $transient_data->response[ $basename ] = $this->_fs->is_plugin() ?
603 $this->_update_details :
604 (array) $this->_update_details;
605 } else {
606 if ( isset( $transient_data->response ) ) {
607 /**
608 * Ensure that there's no update data for the plugin to prevent upgrading the premium version to the latest free version.
609 *
610 * @author Leo Fajardo (@leorw)
611 * @since 2.3.0
612 */
613 unset( $transient_data->response[ $basename ] );
614 }
615
616 if ( ! isset( $transient_data->no_update ) ) {
617 $transient_data->no_update = array();
618 }
619
620 /**
621 * Add product to no_update transient data to properly integrate with WP 5.5 auto-updates UI.
622 *
623 * @since 2.4.1
624 * @link https://make.wordpress.org/core/2020/07/30/recommended-usage-of-the-updates-api-to-support-the-auto-updates-ui-for-plugins-and-themes-in-wordpress-5-5/
625 */
626 $transient_data->no_update[ $basename ] = $this->_fs->is_plugin() ?
627 (object) array(
628 'id' => $basename,
629 'slug' => $this->_fs->get_slug(),
630 'plugin' => $basename,
631 'new_version' => $this->_fs->get_plugin_version(),
632 'url' => '',
633 'package' => '',
634 'icons' => array(),
635 'banners' => array(),
636 'banners_rtl' => array(),
637 'tested' => '',
638 'requires_php' => '',
639 'compatibility' => new stdClass(),
640 ) :
641 array(
642 'theme' => $basename,
643 'new_version' => $this->_fs->get_plugin_version(),
644 'url' => '',
645 'package' => '',
646 'requires' => '',
647 'requires_php' => '',
648 );
649 }
650
651 $slug = $this->_fs->get_slug();
652
653 if ( $this->can_fetch_data_from_wp_org() ) {
654 if ( ! isset( $this->_translation_updates ) ) {
655 $this->_translation_updates = array();
656
657 $translation_updates = $this->fetch_wp_org_module_translation_updates( $module_type, $slug );
658 if ( ! empty( $translation_updates ) ) {
659 $this->_translation_updates = $translation_updates;
660 }
661 }
662
663 if ( ! empty( $this->_translation_updates ) ) {
664 $all_translation_updates = ( isset( $transient_data->translations ) && is_array( $transient_data->translations ) ) ?
665 $transient_data->translations :
666 array();
667
668 $current_plugin_translation_updates_map = array();
669 foreach ( $all_translation_updates as $key => $translation_update ) {
670 if ( $module_type === ( $translation_update['type'] . 's' ) && $slug === $translation_update['slug'] ) {
671 $current_plugin_translation_updates_map[ $translation_update['language'] ] = $translation_update;
672 unset( $all_translation_updates[ $key ] );
673 }
674 }
675
676 foreach ( $this->_translation_updates as $translation_update ) {
677 $lang = $translation_update['language'];
678 if ( ! isset( $current_plugin_translation_updates_map[ $lang ] ) ||
679 version_compare( $translation_update['version'], $current_plugin_translation_updates_map[ $lang ]['version'], '>' )
680 ) {
681 $current_plugin_translation_updates_map[ $lang ] = $translation_update;
682 }
683 }
684
685 $transient_data->translations = array_merge( $all_translation_updates, array_values( $current_plugin_translation_updates_map ) );
686 }
687 }
688
689 return $transient_data;
690 }
691
692 /**
693 * Get module's required data for the updates' mechanism.
694 *
695 * @author Vova Feldman (@svovaf)
696 * @since 2.0.0
697 *
698 * @param \FS_Plugin_Tag $new_version
699 *
700 * @return object
701 */
702 function get_update_details( FS_Plugin_Tag $new_version ) {
703 $update = new stdClass();
704 $update->slug = $this->_fs->get_slug();
705 $update->new_version = $new_version->version;
706 $update->url = WP_FS__ADDRESS;
707 $update->package = $new_version->url;
708 $update->tested = self::get_tested_wp_version( $new_version->tested_up_to_version );
709 $update->requires = $new_version->requires_platform_version;
710 $update->requires_php = $new_version->requires_programming_language_version;
711
712 $icon = $this->_fs->get_local_icon_url();
713
714 if ( ! empty( $icon ) ) {
715 $update->icons = array(
716 // '1x' => $icon,
717 // '2x' => $icon,
718 'default' => $icon,
719 );
720 }
721
722 if ( $this->_fs->is_premium() && ! empty( $new_version->upgrade_notice ) ) {
723 $update->upgrade_notice = $new_version->upgrade_notice;
724 }
725
726 $update->{$this->_fs->get_module_type()} = $this->_fs->get_plugin_basename();
727
728 return $update;
729 }
730
731 /**
732 * @author Leo Fajardo (@leorw)
733 * @since 2.3.0
734 *
735 * @param FS_Plugin_Tag $new_version
736 *
737 * @return bool
738 */
739 private function is_new_version_premium( FS_Plugin_Tag $new_version ) {
740 $params = fs_parse_url_params( $new_version->url );
741
742 return ( isset( $params['is_premium'] ) && 'true' == $params['is_premium'] );
743 }
744
745 /**
746 * Update the updates transient with the module's update information.
747 *
748 * This method is required for multisite environment.
749 * If a module is site activated (not network) and not on the main site,
750 * the module will NOT be executed on the network level, therefore, the
751 * custom updates logic will not be executed as well, so unless we force
752 * the injection of the update into the updates transient, premium updates
753 * will not work.
754 *
755 * @author Vova Feldman (@svovaf)
756 * @since 2.0.0
757 *
758 * @param \FS_Plugin_Tag $new_version
759 */
760 function set_update_data( FS_Plugin_Tag $new_version ) {
761 $this->_logger->entrance();
762
763 if ( ! $this->is_new_version_premium( $new_version ) ) {
764 return;
765 }
766
767 $transient_key = "update_{$this->_fs->get_module_type()}s";
768
769 $transient_data = get_site_transient( $transient_key );
770
771 $transient_data = is_object( $transient_data ) ?
772 $transient_data :
773 new stdClass();
774
775 // Alias.
776 $basename = $this->_fs->get_plugin_basename();
777 $is_plugin = $this->_fs->is_plugin();
778
779 if ( ! isset( $transient_data->response ) ||
780 ! is_array( $transient_data->response )
781 ) {
782 $transient_data->response = array();
783 } else if ( ! empty( $transient_data->response[ $basename ] ) ) {
784 $version = $is_plugin ?
785 ( ! empty( $transient_data->response[ $basename ]->new_version ) ?
786 $transient_data->response[ $basename ]->new_version :
787 null
788 ) : ( ! empty( $transient_data->response[ $basename ]['new_version'] ) ?
789 $transient_data->response[ $basename ]['new_version'] :
790 null
791 );
792
793 if ( $version == $new_version->version ) {
794 // The update data is already set.
795 return;
796 }
797 }
798
799 // Remove the added filters.
800 $this->remove_transient_filters();
801
802 $this->_update_details = $this->get_update_details( $new_version );
803
804 // Set update data in transient.
805 $transient_data->response[ $basename ] = $is_plugin ?
806 $this->_update_details :
807 (array) $this->_update_details;
808
809 if ( ! isset( $transient_data->checked ) ||
810 ! is_array( $transient_data->checked )
811 ) {
812 $transient_data->checked = array();
813 }
814
815 // Flag the module as if it was already checked.
816 $transient_data->checked[ $basename ] = $this->_fs->get_plugin_version();
817 $transient_data->last_checked = time();
818
819 set_site_transient( $transient_key, $transient_data );
820
821 $this->add_transient_filters();
822 }
823
824 /**
825 * @author Leo Fajardo (@leorw)
826 * @since 2.0.2
827 */
828 function delete_update_data() {
829 $this->_logger->entrance();
830
831 $transient_key = "update_{$this->_fs->get_module_type()}s";
832
833 $transient_data = get_site_transient( $transient_key );
834
835 // Alias
836 $basename = $this->_fs->get_plugin_basename();
837
838 if ( ! is_object( $transient_data ) ||
839 ! isset( $transient_data->response ) ||
840 ! is_array( $transient_data->response ) ||
841 empty( $transient_data->response[ $basename ] )
842 ) {
843 return;
844 }
845
846 unset( $transient_data->response[ $basename ] );
847
848 // Remove the added filters.
849 $this->remove_transient_filters();
850
851 set_site_transient( $transient_key, $transient_data );
852
853 $this->add_transient_filters();
854 }
855
856 /**
857 * Try to fetch plugin's info from .org repository.
858 *
859 * @author Vova Feldman (@svovaf)
860 * @since 1.0.5
861 *
862 * @param string $action
863 * @param object $args
864 *
865 * @return bool|mixed
866 */
867 static function _fetch_plugin_info_from_repository( $action, $args ) {
868 $url = $http_url = 'http://api.wordpress.org/plugins/info/1.2/';
869 $url = add_query_arg(
870 array(
871 'action' => $action,
872 'request' => $args,
873 ),
874 $url
875 );
876
877 if ( wp_http_supports( array( 'ssl' ) ) ) {
878 $url = set_url_scheme( $url, 'https' );
879 }
880
881 // The new endpoint version serves only GET requests.
882 $request = wp_remote_get( $url, array( 'timeout' => 15 ) );
883
884 if ( is_wp_error( $request ) ) {
885 return false;
886 }
887
888 $res = json_decode( wp_remote_retrieve_body( $request ), true );
889
890 if ( is_array( $res ) ) {
891 // Object casting is required in order to match the info/1.0 format. We are not decoding directly into an object as we need some fields to remain an array (e.g., $res->sections).
892 $res = (object) $res;
893 }
894
895 if ( ! is_object( $res ) || isset( $res->error ) ) {
896 return false;
897 }
898
899 return $res;
900 }
901
902 /**
903 * Returns true if the product can fetch data from WordPress.org.
904 *
905 * @author Leo Fajardo (@leorw)
906 * @since 2.7.4
907 */
908 private function can_fetch_data_from_wp_org() {
909 return ( $this->_fs->is_org_repo_compliant() && $this->_fs->is_freemium() );
910 }
911
912 /**
913 * Fetches module translation updates from wordpress.org.
914 *
915 * @author Leo Fajardo (@leorw)
916 * @since 2.1.2
917 *
918 * @param string $module_type
919 * @param string $slug
920 *
921 * @return array|null
922 */
923 private function fetch_wp_org_module_translation_updates( $module_type, $slug ) {
924 $plugin_data = $this->_fs->get_plugin_data();
925
926 $locales = array_values( get_available_languages() );
927 $locales = apply_filters( "{$module_type}_update_check_locales", $locales );
928 $locales = array_unique( $locales );
929
930 $plugin_basename = $this->_fs->get_plugin_basename();
931 if ( 'themes' === $module_type ) {
932 $plugin_basename = $slug;
933 }
934
935 global $wp_version;
936
937 $request_args = array(
938 'timeout' => 15,
939 'body' => array(
940 "{$module_type}" => json_encode(
941 array(
942 "{$module_type}" => array(
943 $plugin_basename => array(
944 'Name' => trim( str_replace( $this->_fs->get_plugin()->premium_suffix, '', $plugin_data['Name'] ) ),
945 'Author' => $plugin_data['Author'],
946 )
947 )
948 )
949 ),
950 'translations' => json_encode( $this->get_installed_translations( $module_type, $slug ) ),
951 'locale' => json_encode( $locales )
952 ),
953 'user-agent' => ( 'WordPress/' . $wp_version . '; ' . home_url( '/' ) )
954 );
955
956 $url = "http://api.wordpress.org/{$module_type}/update-check/1.1/";
957 if ( $ssl = wp_http_supports( array( 'ssl' ) ) ) {
958 $url = set_url_scheme( $url, 'https' );
959 }
960
961 $raw_response = Freemius::safe_remote_post(
962 $url,
963 $request_args,
964 WP_FS__TIME_24_HOURS_IN_SEC,
965 WP_FS__TIME_12_HOURS_IN_SEC,
966 false
967 );
968
969 if ( is_wp_error( $raw_response ) ) {
970 return null;
971 }
972
973 $response = json_decode( wp_remote_retrieve_body( $raw_response ), true );
974
975 if ( ! is_array( $response ) ) {
976 return null;
977 }
978
979 if ( ! isset( $response['translations'] ) || empty( $response['translations'] ) ) {
980 return null;
981 }
982
983 return $response['translations'];
984 }
985
986 /**
987 * @author Leo Fajardo (@leorw)
988 * @since 2.1.2
989 *
990 * @param string $module_type
991 * @param string $slug
992 *
993 * @return array
994 */
995 private function get_installed_translations( $module_type, $slug ) {
996 if ( function_exists( 'wp_get_installed_translations' ) ) {
997 return wp_get_installed_translations( $module_type );
998 }
999
1000 $dir = "/{$module_type}";
1001
1002 if ( ! is_dir( WP_LANG_DIR . $dir ) )
1003 return array();
1004
1005 $files = scandir( WP_LANG_DIR . $dir );
1006 if ( ! $files )
1007 return array();
1008
1009 $language_data = array();
1010
1011 foreach ( $files as $file ) {
1012 if ( 0 !== strpos( $file, $slug ) ) {
1013 continue;
1014 }
1015
1016 if ( '.' === $file[0] || is_dir( WP_LANG_DIR . "{$dir}/{$file}" ) ) {
1017 continue;
1018 }
1019
1020 if ( substr( $file, -3 ) !== '.po' ) {
1021 continue;
1022 }
1023
1024 if ( ! preg_match( '/(?:(.+)-)?([a-z]{2,3}(?:_[A-Z]{2})?(?:_[a-z0-9]+)?).po/', $file, $match ) ) {
1025 continue;
1026 }
1027
1028 if ( ! in_array( substr( $file, 0, -3 ) . '.mo', $files ) ) {
1029 continue;
1030 }
1031
1032 list( , $textdomain, $language ) = $match;
1033
1034 if ( '' === $textdomain ) {
1035 $textdomain = 'default';
1036 }
1037
1038 $language_data[ $textdomain ][ $language ] = wp_get_pomo_file_data( WP_LANG_DIR . "{$dir}/{$file}" );
1039 }
1040
1041 return $language_data;
1042 }
1043
1044 /**
1045 * Updates information on the "View version x.x details" page with custom data.
1046 *
1047 * @author Vova Feldman (@svovaf)
1048 * @since 1.0.4
1049 *
1050 * @uses FS_Api
1051 *
1052 * @param object $data
1053 * @param string $action
1054 * @param mixed $args
1055 *
1056 * @return object
1057 */
1058 function plugins_api_filter( $data, $action = '', $args = null ) {
1059 $this->_logger->entrance();
1060
1061 if ( ( 'plugin_information' !== $action ) ||
1062 ! isset( $args->slug )
1063 ) {
1064 return $data;
1065 }
1066
1067 $addon = false;
1068 $is_addon = false;
1069 $addon_version = false;
1070
1071 if ( $this->_fs->get_slug() !== $args->slug ) {
1072 $addon = $this->_fs->get_addon_by_slug( $args->slug );
1073
1074 if ( ! is_object( $addon ) ) {
1075 return $data;
1076 }
1077
1078 if ( $this->_fs->is_addon_activated( $addon->id ) ) {
1079 $addon_version = $this->_fs->get_addon_instance( $addon->id )->get_plugin_version();
1080 } else if ( $this->_fs->is_addon_installed( $addon->id ) ) {
1081 $addon_plugin_data = get_plugin_data(
1082 ( WP_PLUGIN_DIR . '/' . $this->_fs->get_addon_basename( $addon->id ) ),
1083 false,
1084 false
1085 );
1086
1087 if ( ! empty( $addon_plugin_data ) ) {
1088 $addon_version = $addon_plugin_data['Version'];
1089 }
1090 }
1091
1092 $is_addon = true;
1093 }
1094
1095 $plugin_in_repo = false;
1096 if ( ! $is_addon && $this->can_fetch_data_from_wp_org() ) {
1097 // Try to fetch info from .org repository.
1098 $data = self::_fetch_plugin_info_from_repository( $action, $args );
1099
1100 $plugin_in_repo = ( false !== $data );
1101 }
1102
1103 if ( ! $plugin_in_repo ) {
1104 $data = $args;
1105
1106 // Fetch as much as possible info from local files.
1107 $plugin_local_data = $this->_fs->get_plugin_data();
1108 $data->name = $plugin_local_data['Name'];
1109 $data->author = $plugin_local_data['Author'];
1110 $data->sections = array(
1111 'description' => 'Upgrade ' . $plugin_local_data['Name'] . ' to latest.',
1112 );
1113
1114 // @todo Store extra plugin info on Freemius or parse readme.txt markup.
1115 /*$info = $this->_fs->get_api_site_scope()->call('/information.json');
1116
1117 if ( !isset($info->error) ) {
1118 $data = $info;
1119 }*/
1120 }
1121
1122 $plugin_version = $is_addon ?
1123 $addon_version :
1124 $this->_fs->get_plugin_version();
1125
1126 // Get plugin's newest update.
1127 $new_version = $this->get_latest_download_details( $is_addon ? $addon->id : false, $plugin_version );
1128
1129 if ( ! is_object( $new_version ) || empty( $new_version->version ) ) {
1130 $data->version = $plugin_version;
1131 } else {
1132 if ( $is_addon ) {
1133 $data->name = $addon->title . ' ' . $this->_fs->get_text_inline( 'Add-On', 'addon' );
1134 $data->slug = $addon->slug;
1135 $data->url = WP_FS__ADDRESS;
1136 $data->package = $new_version->url;
1137 }
1138
1139 if ( ! $plugin_in_repo ) {
1140 $data->last_updated = ! is_null( $new_version->updated ) ? $new_version->updated : $new_version->created;
1141 $data->requires = $new_version->requires_platform_version;
1142 $data->requires_php = $new_version->requires_programming_language_version;
1143 $data->tested = $new_version->tested_up_to_version;
1144 }
1145
1146 $data->version = $new_version->version;
1147 $data->download_link = $new_version->url;
1148
1149 if ( isset( $new_version->readme ) && is_object( $new_version->readme ) ) {
1150 $new_version_readme_data = $new_version->readme;
1151 if ( isset( $new_version_readme_data->sections ) ) {
1152 $new_version_readme_data->sections = (array) $new_version_readme_data->sections;
1153 } else {
1154 $new_version_readme_data->sections = array();
1155 }
1156
1157 if ( isset( $data->sections ) ) {
1158 if ( isset( $data->sections['screenshots'] ) ) {
1159 $new_version_readme_data->sections['screenshots'] = $data->sections['screenshots'];
1160 }
1161
1162 if ( isset( $data->sections['reviews'] ) ) {
1163 $new_version_readme_data->sections['reviews'] = $data->sections['reviews'];
1164 }
1165 }
1166
1167 if ( isset( $new_version_readme_data->banners ) ) {
1168 $new_version_readme_data->banners = (array) $new_version_readme_data->banners;
1169 } else if ( isset( $data->banners ) ) {
1170 $new_version_readme_data->banners = $data->banners;
1171 }
1172
1173 $wp_org_sections = array(
1174 'author',
1175 'author_profile',
1176 'rating',
1177 'ratings',
1178 'num_ratings',
1179 'support_threads',
1180 'support_threads_resolved',
1181 'active_installs',
1182 'added',
1183 'homepage'
1184 );
1185
1186 foreach ( $wp_org_sections as $wp_org_section ) {
1187 if ( isset( $data->{$wp_org_section} ) ) {
1188 $new_version_readme_data->{$wp_org_section} = $data->{$wp_org_section};
1189 }
1190 }
1191
1192 $data = $new_version_readme_data;
1193 }
1194 }
1195
1196 if ( ! empty( $data->tested ) ) {
1197 $data->tested = self::get_tested_wp_version( $data->tested );
1198 }
1199
1200 return $data;
1201 }
1202
1203 /**
1204 * @since 2.5.3 If the current WordPress version is a patch of the tested version (e.g., 6.1.2 is a patch of 6.1), then override the tested version with the patch so developers won't need to release a new version just to bump the latest supported WP version.
1205 *
1206 * @param string|null $tested_up_to
1207 *
1208 * @return string|null
1209 */
1210 private static function get_tested_wp_version( $tested_up_to ) {
1211 $current_wp_version = get_bloginfo( 'version' );
1212
1213 return ( ! empty($tested_up_to) && fs_starts_with( $current_wp_version, $tested_up_to . '.' ) ) ?
1214 $current_wp_version :
1215 $tested_up_to;
1216 }
1217
1218 /**
1219 * @author Vova Feldman (@svovaf)
1220 * @since 1.2.1.7
1221 *
1222 * @param number|bool $addon_id
1223 * @param bool|string $newer_than Since 2.2.1
1224 * @param bool|string $fetch_readme Since 2.2.1
1225 *
1226 * @return object
1227 */
1228 private function get_latest_download_details( $addon_id = false, $newer_than = false, $fetch_readme = true ) {
1229 return $this->_fs->_fetch_latest_version( $addon_id, true, FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION, $newer_than, $fetch_readme );
1230 }
1231
1232 /**
1233 * Checks if a given basename has a matching folder name
1234 * with the current context plugin.
1235 *
1236 * @author Vova Feldman (@svovaf)
1237 * @since 1.2.1.6
1238 *
1239 * @return bool
1240 */
1241 private function is_correct_folder_name() {
1242 return ( $this->_fs->get_target_folder_name() == trim( dirname( $this->_fs->get_plugin_basename() ), '/\\' ) );
1243 }
1244
1245 /**
1246 * This is a special after upgrade handler for migrating modules
1247 * that didn't use the '-premium' suffix folder structure before
1248 * the migration.
1249 *
1250 * @author Vova Feldman (@svovaf)
1251 * @since 1.2.1.6
1252 *
1253 * @param bool $response Install response.
1254 * @param array $hook_extra Extra arguments passed to hooked filters.
1255 * @param array $result Installation result data.
1256 *
1257 * @return bool
1258 */
1259 function _maybe_update_folder_name( $response, $hook_extra, $result ) {
1260 $basename = $this->_fs->get_plugin_basename();
1261
1262 if ( true !== $response ||
1263 empty( $hook_extra ) ||
1264 empty( $hook_extra['plugin'] ) ||
1265 $basename !== $hook_extra['plugin']
1266 ) {
1267 return $response;
1268 }
1269
1270 $active_plugins_basenames = get_option( 'active_plugins' );
1271
1272 foreach ( $active_plugins_basenames as $key => $active_plugin_basename ) {
1273 if ( $basename === $active_plugin_basename ) {
1274 // Get filename including extension.
1275 $filename = basename( $basename );
1276
1277 $new_basename = plugin_basename(
1278 trailingslashit( $this->_fs->is_premium() ? $this->_fs->get_premium_slug() : $this->_fs->get_slug() ) .
1279 $filename
1280 );
1281
1282 // Verify that the expected correct path exists.
1283 if ( file_exists( fs_normalize_path( WP_PLUGIN_DIR . '/' . $new_basename ) ) ) {
1284 // Override active plugin name.
1285 $active_plugins_basenames[ $key ] = $new_basename;
1286 update_option( 'active_plugins', $active_plugins_basenames );
1287 }
1288
1289 break;
1290 }
1291 }
1292
1293 return $response;
1294 }
1295
1296 #----------------------------------------------------------------------------------
1297 #region Auto Activation
1298 #----------------------------------------------------------------------------------
1299
1300 /**
1301 * Installs and active a plugin when explicitly requested that from a 3rd party service.
1302 *
1303 * This logic was inspired by the TGMPA GPL licensed library by Thomas Griffin.
1304 *
1305 * @link http://tgmpluginactivation.com/
1306 *
1307 * @author Vova Feldman
1308 * @since 1.2.1.7
1309 *
1310 * @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
1311 *
1312 * @uses WP_Filesystem
1313 * @uses WP_Error
1314 * @uses WP_Upgrader
1315 * @uses Plugin_Upgrader
1316 * @uses Plugin_Installer_Skin
1317 * @uses Plugin_Upgrader_Skin
1318 *
1319 * @param number|bool $plugin_id
1320 *
1321 * @return array
1322 */
1323 function install_and_activate_plugin( $plugin_id = false ) {
1324 if ( ! empty( $plugin_id ) && ! FS_Plugin::is_valid_id( $plugin_id ) ) {
1325 // Invalid plugin ID.
1326 return array(
1327 'message' => $this->_fs->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
1328 'code' => 'invalid_module_id',
1329 );
1330 }
1331
1332 $is_addon = false;
1333 if ( FS_Plugin::is_valid_id( $plugin_id ) &&
1334 $plugin_id != $this->_fs->get_id()
1335 ) {
1336 $addon = $this->_fs->get_addon( $plugin_id );
1337
1338 if ( ! is_object( $addon ) ) {
1339 // Invalid add-on ID.
1340 return array(
1341 'message' => $this->_fs->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
1342 'code' => 'invalid_module_id',
1343 );
1344 }
1345
1346 $slug = $addon->slug;
1347 $premium_slug = $addon->premium_slug;
1348 $title = $addon->title . ' ' . $this->_fs->get_text_inline( 'Add-On', 'addon' );
1349
1350 $is_addon = true;
1351 } else {
1352 $slug = $this->_fs->get_slug();
1353 $premium_slug = $this->_fs->get_premium_slug();
1354 $title = $this->_fs->get_plugin_title() .
1355 ( $this->_fs->is_addon() ? ' ' . $this->_fs->get_text_inline( 'Add-On', 'addon' ) : '' );
1356 }
1357
1358 if ( $this->is_premium_plugin_active( $plugin_id ) ) {
1359 // Premium version already activated.
1360 return array(
1361 'message' => $is_addon ?
1362 $this->_fs->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ) :
1363 $this->_fs->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
1364 'code' => 'premium_installed',
1365 );
1366 }
1367
1368 $latest_version = $this->get_latest_download_details( $plugin_id, false, false );
1369 $target_folder = $premium_slug;
1370
1371 // Prep variables for Plugin_Installer_Skin class.
1372 $extra = array();
1373 $extra['slug'] = $target_folder;
1374 $source = $latest_version->url;
1375 $api = null;
1376
1377 $install_url = add_query_arg(
1378 array(
1379 'action' => 'install-plugin',
1380 'plugin' => urlencode( $slug ),
1381 ),
1382 'update.php'
1383 );
1384
1385 if ( ! class_exists( 'Plugin_Upgrader', false ) ) {
1386 // Include required resources for the installation.
1387 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
1388 }
1389
1390 $skin_args = array(
1391 'type' => 'web',
1392 'title' => sprintf( $this->_fs->get_text_inline( 'Installing plugin: %s', 'installing-plugin-x' ), $title ),
1393 'url' => esc_url_raw( $install_url ),
1394 'nonce' => 'install-plugin_' . $slug,
1395 'plugin' => '',
1396 'api' => $api,
1397 'extra' => $extra,
1398 );
1399
1400 // $skin = new Automatic_Upgrader_Skin( $skin_args );
1401 // $skin = new Plugin_Installer_Skin( $skin_args );
1402 $skin = new WP_Ajax_Upgrader_Skin( $skin_args );
1403
1404 // Create a new instance of Plugin_Upgrader.
1405 $upgrader = new Plugin_Upgrader( $skin );
1406
1407 // Perform the action and install the plugin from the $source urldecode().
1408 add_filter( 'upgrader_source_selection', array( 'FS_Plugin_Updater', '_maybe_adjust_source_dir' ), 1, 3 );
1409
1410 $install_result = $upgrader->install( $source );
1411
1412 remove_filter( 'upgrader_source_selection', array( 'FS_Plugin_Updater', '_maybe_adjust_source_dir' ), 1 );
1413
1414 if ( is_wp_error( $install_result ) ) {
1415 return array(
1416 'message' => $install_result->get_error_message(),
1417 'code' => $install_result->get_error_code(),
1418 );
1419 } elseif ( is_wp_error( $skin->result ) ) {
1420 return array(
1421 'message' => $skin->result->get_error_message(),
1422 'code' => $skin->result->get_error_code(),
1423 );
1424 } elseif ( $skin->get_errors()->get_error_code() ) {
1425 return array(
1426 'message' => $skin->get_error_messages(),
1427 'code' => 'unknown',
1428 );
1429 } elseif ( is_null( $install_result ) ) {
1430 global $wp_filesystem;
1431
1432 $error_code = 'unable_to_connect_to_filesystem';
1433 $error_message = $this->_fs->get_text_inline( 'Unable to connect to the filesystem. Please confirm your credentials.' );
1434
1435 // Pass through the error from WP_Filesystem if one was raised.
1436 if ( $wp_filesystem instanceof WP_Filesystem_Base &&
1437 is_wp_error( $wp_filesystem->errors ) &&
1438 $wp_filesystem->errors->get_error_code()
1439 ) {
1440 $error_message = $wp_filesystem->errors->get_error_message();
1441 }
1442
1443 return array(
1444 'message' => $error_message,
1445 'code' => $error_code,
1446 );
1447 }
1448
1449 // Grab the full path to the main plugin's file.
1450 $plugin_activate = $upgrader->plugin_info();
1451
1452 // Try to activate the plugin.
1453 $activation_result = $this->try_activate_plugin( $plugin_activate );
1454
1455 if ( is_wp_error( $activation_result ) ) {
1456 return array(
1457 'message' => $activation_result->get_error_message(),
1458 'code' => $activation_result->get_error_code(),
1459 );
1460 }
1461
1462 return $skin->get_upgrade_messages();
1463 }
1464
1465 /**
1466 * Tries to activate a plugin. If fails, returns the error.
1467 *
1468 * @author Vova Feldman
1469 * @since 1.2.1.7
1470 *
1471 * @param string $file_path Path within wp-plugins/ to main plugin file.
1472 * This determines the styling of the output messages.
1473 *
1474 * @return bool|WP_Error
1475 */
1476 protected function try_activate_plugin( $file_path ) {
1477 $activate = activate_plugin( $file_path, '', $this->_fs->is_network_active() );
1478
1479 return is_wp_error( $activate ) ?
1480 $activate :
1481 true;
1482 }
1483
1484 /**
1485 * Check if a premium module version is already active.
1486 *
1487 * @author Vova Feldman
1488 * @since 1.2.1.7
1489 *
1490 * @param number|bool $plugin_id
1491 *
1492 * @return bool
1493 */
1494 private function is_premium_plugin_active( $plugin_id = false ) {
1495 if ( $plugin_id != $this->_fs->get_id() ) {
1496 return $this->_fs->is_addon_activated( $plugin_id, true );
1497 }
1498
1499 return is_plugin_active( $this->_fs->premium_plugin_basename() );
1500 }
1501
1502 /**
1503 * Store the basename since it's not always available in the `_maybe_adjust_source_dir` method below.
1504 *
1505 * @author Leo Fajardo (@leorw)
1506 * @since 2.2.1
1507 *
1508 * @param bool|WP_Error $response Response.
1509 * @param array $hook_extra Extra arguments passed to hooked filters.
1510 *
1511 * @return bool|WP_Error
1512 */
1513 static function _store_basename_for_source_adjustment( $response, $hook_extra ) {
1514 if ( isset( $hook_extra['plugin'] ) ) {
1515 self::$_upgrade_basename = $hook_extra['plugin'];
1516 } else if ( isset( $hook_extra['theme'] ) ) {
1517 self::$_upgrade_basename = $hook_extra['theme'];
1518 } else {
1519 self::$_upgrade_basename = null;
1520 }
1521
1522 return $response;
1523 }
1524
1525 /**
1526 * Adjust the plugin directory name if necessary.
1527 * Assumes plugin has a folder (not a single file plugin).
1528 *
1529 * The final destination directory of a plugin is based on the subdirectory name found in the
1530 * (un)zipped source. In some cases this subdirectory name is not the same as the expected
1531 * slug and the plugin will not be recognized as installed. This is fixed by adjusting
1532 * the temporary unzipped source subdirectory name to the expected plugin slug.
1533 *
1534 * @author Vova Feldman
1535 * @since 1.2.1.7
1536 * @since 2.2.1 The method was converted to static since when the admin update bulk products via the Updates section, the logic applies the `upgrader_source_selection` filter for every product that is being updated.
1537 *
1538 * @param string $source Path to upgrade/zip-file-name.tmp/subdirectory/.
1539 * @param string $remote_source Path to upgrade/zip-file-name.tmp.
1540 * @param \WP_Upgrader $upgrader Instance of the upgrader which installs the plugin.
1541 *
1542 * @return string|WP_Error
1543 */
1544 static function _maybe_adjust_source_dir( $source, $remote_source, $upgrader ) {
1545 if ( ! is_object( $GLOBALS['wp_filesystem'] ) ) {
1546 return $source;
1547 }
1548
1549 $basename = self::$_upgrade_basename;
1550 $is_theme = false;
1551
1552 // Figure out what the slug is supposed to be.
1553 if ( isset( $upgrader->skin->options['extra'] ) ) {
1554 // Set by the auto-install logic.
1555 $desired_slug = $upgrader->skin->options['extra']['slug'];
1556 } else if ( ! empty( $basename ) ) {
1557 /**
1558 * If it doesn't end with ".php", it's a theme.
1559 *
1560 * @author Leo Fajardo (@leorw)
1561 * @since 2.2.1
1562 */
1563 $is_theme = ( ! fs_ends_with( $basename, '.php' ) );
1564
1565 $desired_slug = ( ! $is_theme ) ?
1566 dirname( $basename ) :
1567 // Theme slug
1568 $basename;
1569 } else {
1570 // Can't figure out the desired slug, stop the execution.
1571 return $source;
1572 }
1573
1574 if ( is_multisite() ) {
1575 /**
1576 * If we are running in a multisite environment and the product is not network activated,
1577 * the instance will not exist anyway. Therefore, try to update the source if necessary
1578 * regardless if the Freemius instance of the product exists or not.
1579 *
1580 * @author Vova Feldman
1581 */
1582 } else if ( ! empty( $basename ) ) {
1583 $fs = Freemius::get_instance_by_file(
1584 $basename,
1585 $is_theme ?
1586 WP_FS__MODULE_TYPE_THEME :
1587 WP_FS__MODULE_TYPE_PLUGIN
1588 );
1589
1590 if ( ! is_object( $fs ) ) {
1591 /**
1592 * If the Freemius instance does not exist on a non-multisite network environment, it means that:
1593 * 1. The product is not powered by Freemius; OR
1594 * 2. The product is not activated, therefore, we don't mind if after the update the folder name will change.
1595 *
1596 * @author Leo Fajardo (@leorw)
1597 * @since 2.2.1
1598 */
1599 return $source;
1600 }
1601 }
1602
1603 $subdir_name = untrailingslashit( str_replace( trailingslashit( $remote_source ), '', $source ) );
1604
1605 if ( ! empty( $subdir_name ) && $subdir_name !== $desired_slug ) {
1606 $from_path = untrailingslashit( $source );
1607 $to_path = trailingslashit( $remote_source ) . $desired_slug;
1608
1609 if ( true === $GLOBALS['wp_filesystem']->move( $from_path, $to_path ) ) {
1610 return trailingslashit( $to_path );
1611 }
1612
1613 return new WP_Error(
1614 'rename_failed',
1615 fs_text_inline( 'The remote plugin package does not contain a folder with the desired slug and renaming did not work.', 'module-package-rename-failure' ),
1616 array(
1617 'found' => $subdir_name,
1618 'expected' => $desired_slug
1619 )
1620 );
1621 }
1622
1623 return $source;
1624 }
1625
1626 #endregion
1627 }
1628