PluginProbe
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites / 2.6.1
BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites v2.6.1
4.1.0 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.2 1.2.3 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 2.1.2 2.5.0 2.5.3 2.6.1 All 38 releases
blockspare / freemius / includes / class-fs-plugin-updater.php

class-fs-plugin-updater.php in BlockSpare – Gutenberg Blocks for News, Magazine, Blog & Business Websites 2.6.1, at freemius/includes/class-fs-plugin-updater.php

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