--- a/post-snippets/freemius/includes/class-freemius.php
+++ b/post-snippets/freemius/includes/class-freemius.php
@@ -3629,7 +3629,7 @@
$this->delete_current_install( false );
- $license_key = false;
+ $license = null;
if (
is_object( $this->_license ) &&
@@ -3637,20 +3637,21 @@
( WP_FS__IS_LOCALHOST_FOR_SERVER || FS_Site::is_localhost_by_address( self::get_unfiltered_site_url() ) )
)
) {
- $license_key = $this->_license->secret_key;
+ $license = $this->_license;
}
return $this->opt_in(
false,
false,
false,
- $license_key,
+ ( is_object( $license ) ? $license->secret_key : false ),
false,
false,
false,
null,
array(),
- false
+ false,
+ ( is_object( $license ) ? $license->user_id : null )
);
}
@@ -4494,33 +4495,31 @@
return;
}
- if ( $this->has_api_connectivity() ) {
- if ( self::is_cron() ) {
- $this->hook_callback_to_sync_cron();
- } else if ( $this->is_user_in_admin() ) {
- /**
- * Schedule daily data sync cron if:
- *
- * 1. User opted-in (for tracking).
- * 2. If skipped, but later upgraded (opted-in via upgrade).
- *
- * @author Vova Feldman (@svovaf)
- * @since 1.1.7.3
- *
- */
- if ( $this->is_registered() && $this->is_tracking_allowed() ) {
- $this->maybe_schedule_sync_cron();
- }
+ $this->hook_callback_to_sync_cron();
- /**
- * Check if requested for manual blocking background sync.
- */
- if ( fs_request_has( 'background_sync' ) ) {
- self::require_pluggable_essentials();
- self::wp_cookie_constants();
+ if ( $this->has_api_connectivity() && ! self::is_cron() && $this->is_user_in_admin() ) {
+ /**
+ * Schedule daily data sync cron if:
+ *
+ * 1. User opted-in (for tracking).
+ * 2. If skipped, but later upgraded (opted-in via upgrade).
+ *
+ * @author Vova Feldman (@svovaf)
+ * @since 1.1.7.3
+ *
+ */
+ if ( $this->is_registered() && $this->is_tracking_allowed() ) {
+ $this->maybe_schedule_sync_cron();
+ }
- $this->run_manual_sync();
- }
+ /**
+ * Check if requested for manual blocking background sync.
+ */
+ if ( fs_request_has( 'background_sync' ) ) {
+ self::require_pluggable_essentials();
+ self::wp_cookie_constants();
+
+ $this->run_manual_sync();
}
}
@@ -7659,7 +7658,14 @@
$parent_fs->get_current_or_network_user()->email,
false,
false,
- $license->secret_key
+ $license->secret_key,
+ false,
+ false,
+ false,
+ null,
+ array(),
+ true,
+ $license->user_id
);
} else {
// Activate the license.
@@ -7723,7 +7729,9 @@
false,
false,
null,
- $sites
+ $sites,
+ true,
+ $license->user_id
);
} else {
$blog_2_install_map = array();
@@ -7777,7 +7785,7 @@
* @param array $sites
* @param int $blog_id
*/
- private function maybe_activate_bundle_license( FS_Plugin_License $license = null, $sites = array(), $blog_id = 0 ) {
+ private function maybe_activate_bundle_license( $license = null, $sites = array(), $blog_id = 0 ) {
if ( ! is_object( $license ) && $this->has_active_valid_license() ) {
$license = $this->_license;
}
@@ -7949,7 +7957,8 @@
null,
null,
$sites,
- ( $current_blog_id > 0 ? $current_blog_id : null )
+ ( $current_blog_id > 0 ? $current_blog_id : null ),
+ $license->user_id
);
}
}
@@ -8830,8 +8839,13 @@
isset( $site_active_plugins[ $basename ] )
) {
// Plugin was site level activated.
- $site_active_plugins_cache->plugins[ $basename ] = $network_plugins[ $basename ];
- $site_active_plugins_cache->plugins[ $basename ]['is_active'] = true;
+ $site_active_plugins_cache->plugins[ $basename ] = array(
+ 'slug' => $network_plugins[ $basename ]['slug'],
+ 'version' => $network_plugins[ $basename ]['Version'],
+ 'title' => $network_plugins[ $basename ]['Name'],
+ 'is_active' => $is_active,
+ 'is_uninstalled' => false,
+ );
} else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
! isset( $site_active_plugins[ $basename ] )
) {
@@ -11576,7 +11590,7 @@
continue;
}
- $missing_plan = self::_get_plan_by_id( $plan_id );
+ $missing_plan = self::_get_plan_by_id( $plan_id, false );
if ( is_object( $missing_plan ) ) {
$plans[] = $missing_plan;
@@ -11738,10 +11752,10 @@
*
* @return FS_Plugin_Plan|false
*/
- function _get_plan_by_id( $id ) {
+ function _get_plan_by_id( $id, $allow_sync = true ) {
$this->_logger->entrance();
- if ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) {
+ if ( $allow_sync && ( ! is_array( $this->_plans ) || 0 === count( $this->_plans ) ) ) {
$this->_sync_plans();
}
@@ -12385,7 +12399,7 @@
*
* @param FS_Plugin_License $license
*/
- private function set_license( FS_Plugin_License $license = null ) {
+ private function set_license( $license = null ) {
$this->_license = $license;
$this->maybe_update_whitelabel_flag( $license );
@@ -13485,7 +13499,8 @@
fs_request_get( 'module_id', null, 'post' ),
fs_request_get( 'user_id', null ),
fs_request_get_bool( 'is_extensions_tracking_allowed', null ),
- fs_request_get_bool( 'is_diagnostic_tracking_allowed', null )
+ fs_request_get_bool( 'is_diagnostic_tracking_allowed', null ),
+ fs_request_get( 'license_owner_id', null )
);
if (
@@ -13634,6 +13649,7 @@
* @param null|number $plugin_id
* @param array $sites
* @param int $blog_id
+ * @param null|number $license_owner_id
*
* @return array {
* @var bool $success
@@ -13648,7 +13664,8 @@
$is_marketing_allowed = null,
$plugin_id = null,
$sites = array(),
- $blog_id = null
+ $blog_id = null,
+ $license_owner_id = null
) {
$this->_logger->entrance();
@@ -13659,7 +13676,11 @@
$sites,
$is_marketing_allowed,
$blog_id,
- $plugin_id
+ $plugin_id,
+ null,
+ null,
+ null,
+ $license_owner_id
);
// No need to show the sticky after license activation notice after migrating a license.
@@ -13733,9 +13754,10 @@
* @param null|bool $is_marketing_allowed
* @param null|int $blog_id
* @param null|number $plugin_id
- * @param null|number $license_owner_id
+ * @param null|number $user_id
* @param bool|null $is_extensions_tracking_allowed
* @param bool|null $is_diagnostic_tracking_allowed Since 2.5.0.2 to allow license activation with minimal data footprint.
+ * @param null|number $license_owner_id
*
*
* @return array {
@@ -13750,9 +13772,10 @@
$is_marketing_allowed = null,
$blog_id = null,
$plugin_id = null,
- $license_owner_id = null,
+ $user_id = null,
$is_extensions_tracking_allowed = null,
- $is_diagnostic_tracking_allowed = null
+ $is_diagnostic_tracking_allowed = null,
+ $license_owner_id = null
) {
$this->_logger->entrance();
@@ -13841,10 +13864,10 @@
$install_ids = array();
- $change_owner = FS_User::is_valid_id( $license_owner_id );
+ $change_owner = FS_User::is_valid_id( $user_id );
if ( $change_owner ) {
- $params['user_id'] = $license_owner_id;
+ $params['user_id'] = $user_id;
$installs_info_by_slug_map = $fs->get_parent_and_addons_installs_info();
@@ -13920,7 +13943,9 @@
false,
false,
$is_marketing_allowed,
- $sites
+ $sites,
+ true,
+ $license_owner_id
);
if ( isset( $next_page->error ) ) {
@@ -14009,6 +14034,10 @@
$result['next_page'] = $next_page;
}
+ if ( $result['success'] ) {
+ $this->do_action( 'after_license_activation' );
+ }
+
return $result;
}
@@ -15630,7 +15659,7 @@
*
* @return bool Since 2.3.1 returns if a switch was made.
*/
- function switch_to_blog( $blog_id, FS_Site $install = null, $flush = false ) {
+ function switch_to_blog( $blog_id, $install = null, $flush = false ) {
if ( ! is_numeric( $blog_id ) ) {
return false;
}
@@ -15757,6 +15786,10 @@
function get_site_info( $site = null, $load_registration = false ) {
$this->_logger->entrance();
+ $fs_hook_snapshot = new FS_Hook_Snapshot();
+ // Remove all filters from `switch_blog`.
+ $fs_hook_snapshot->remove( 'switch_blog' );
+
$switched = false;
$registration_date = null;
@@ -15816,6 +15849,9 @@
restore_current_blog();
}
+ // Add the filters back to `switch_blog`.
+ $fs_hook_snapshot->restore( 'switch_blog' );
+
return $info;
}
@@ -16936,14 +16972,13 @@
*
* @param array $override_with
* @param bool|int|null $network_level_or_blog_id If true, return params for network level opt-in. If integer, get params for specified blog in the network.
+ * @param bool $skip_user_info
*
* @return array
*/
- function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
+ function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null, $skip_user_info = false ) {
$this->_logger->entrance();
- $current_user = self::_get_current_wp_user();
-
$activation_action = $this->get_unique_affix() . '_activate_new';
$return_url = $this->is_anonymous() ?
// If skipped already, then return to the account page.
@@ -16954,9 +16989,6 @@
$versions = $this->get_versions();
$params = array_merge( $versions, array(
- 'user_firstname' => $current_user->user_firstname,
- 'user_lastname' => $current_user->user_lastname,
- 'user_email' => $current_user->user_email,
'plugin_slug' => $this->_slug,
'plugin_id' => $this->get_id(),
'plugin_public_key' => $this->get_public_key(),
@@ -16972,6 +17004,21 @@
'is_localhost' => WP_FS__IS_LOCALHOST,
) );
+ if (
+ ! $skip_user_info &&
+ (
+ empty( $override_with['user_firstname'] ) ||
+ empty( $override_with['user_lastname'] ) ||
+ empty( $override_with['user_email'] )
+ )
+ ) {
+ $current_user = self::_get_current_wp_user();
+
+ $params['user_firstname'] = $current_user->user_firstname;
+ $params['user_lastname'] = $current_user->user_lastname;
+ $params['user_email'] = $current_user->user_email;
+ }
+
if ( $this->is_addon() ) {
$parent_fs = $this->get_parent_instance();
@@ -17051,6 +17098,7 @@
* @param null|bool $is_marketing_allowed
* @param array $sites If network-level opt-in, an array of containing details of sites.
* @param bool $redirect
+ * @param null|number $license_owner_id
*
* @return string|object
* @use WP_Error
@@ -17065,15 +17113,11 @@
$is_disconnected = false,
$is_marketing_allowed = null,
$sites = array(),
- $redirect = true
+ $redirect = true,
+ $license_owner_id = null
) {
$this->_logger->entrance();
- if ( false === $email ) {
- $current_user = self::_get_current_wp_user();
- $email = $current_user->user_email;
- }
-
/**
* @since 1.2.1 If activating with license key, ignore the context-user
* since the user will be automatically loaded from the license.
@@ -17083,6 +17127,11 @@
$this->_storage->remove( 'pending_license_key' );
if ( ! $is_uninstall ) {
+ if ( false === $email ) {
+ $current_user = self::_get_current_wp_user();
+ $email = $current_user->user_email;
+ }
+
$fs_user = Freemius::_get_user_by_email( $email );
if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
return $this->install_with_user(
@@ -17097,15 +17146,22 @@
}
}
+ $skip_user_info = ( ! empty( $license_key ) && FS_User::is_valid_id( $license_owner_id ) );
+
$user_info = array();
- if ( ! empty( $email ) ) {
- $user_info['user_email'] = $email;
- }
- if ( ! empty( $first ) ) {
- $user_info['user_firstname'] = $first;
- }
- if ( ! empty( $last ) ) {
- $user_info['user_lastname'] = $last;
+
+ if ( ! $skip_user_info ) {
+ if ( ! empty( $email ) ) {
+ $user_info['user_email'] = $email;
+ }
+
+ if ( ! empty( $first ) ) {
+ $user_info['user_firstname'] = $first;
+ }
+
+ if ( ! empty( $last ) ) {
+ $user_info['user_lastname'] = $last;
+ }
}
if ( ! empty( $sites ) ) {
@@ -17116,7 +17172,7 @@
$is_network = false;
}
- $params = $this->get_opt_in_params( $user_info, $is_network );
+ $params = $this->get_opt_in_params( $user_info, $is_network, $skip_user_info );
$filtered_license_key = false;
if ( is_string( $license_key ) ) {
@@ -18112,7 +18168,7 @@
private function _activate_addon_account(
Freemius $parent_fs,
$network_level_or_blog_id = null,
- FS_Plugin_License $bundle_license = null
+ $bundle_license = null
) {
if ( $this->is_registered() ) {
// Already activated.
@@ -18745,7 +18801,7 @@
* @return bool
*/
function is_pricing_page_visible() {
- return (
+ $visible = (
// Has at least one paid plan.
$this->has_paid_plan() &&
// Didn't ask to hide the pricing page.
@@ -18753,6 +18809,8 @@
// Don't have a valid active license or has more than one plan.
( ! $this->is_paying() || ! $this->is_single_plan( true ) )
);
+
+ return $this->apply_filters( 'is_pricing_page_visible', $visible );
}
/**
@@ -19708,7 +19766,7 @@
* @param null|int $network_level_or_blog_id Since 2.0.0
* @param FS_Site $site Since 2.0.0
*/
- private function _store_site( $store = true, $network_level_or_blog_id = null, FS_Site $site = null, $is_backup = false ) {
+ private function _store_site( $store = true, $network_level_or_blog_id = null, $site = null, $is_backup = false ) {
$this->_logger->entrance();
if ( is_null( $site ) ) {
@@ -20561,11 +20619,18 @@
* @param bool $flush Since 1.1.7.3
* @param int $expiration Since 1.2.2.7
* @param bool|string $newer_than Since 2.2.1
+ * @param bool $fetch_upgrade_notice Since 2.12.1
*
* @return object|false New plugin tag info if exist.
*/
- private function _fetch_newer_version( $plugin_id = false, $flush = true, $expiration = WP_FS__TIME_24_HOURS_IN_SEC, $newer_than = false ) {
- $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than );
+ private function _fetch_newer_version(
+ $plugin_id = false,
+ $flush = true,
+ $expiration = WP_FS__TIME_24_HOURS_IN_SEC,
+ $newer_than = false,
+ $fetch_upgrade_notice = true
+ ) {
+ $latest_tag = $this->_fetch_latest_version( $plugin_id, $flush, $expiration, $newer_than, false, $fetch_upgrade_notice );
if ( ! is_object( $latest_tag ) ) {
return false;
@@ -20598,19 +20663,18 @@
*
* @param bool|number $plugin_id
* @param bool $flush Since 1.1.7.3
- * @param int $expiration Since 1.2.2.7
- * @param bool|string $newer_than Since 2.2.1
*
* @return bool|FS_Plugin_Tag
*/
- function get_update( $plugin_id = false, $flush = true, $expiration = FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION, $newer_than = false ) {
+ function get_update( $plugin_id = false, $flush = true ) {
$this->_logger->entrance();
if ( ! is_numeric( $plugin_id ) ) {
$plugin_id = $this->_plugin->id;
}
- $this->check_updates( true, $plugin_id, $flush, $expiration, $newer_than );
+ $this->check_updates( true, $plugin_id, $flush );
+
$updates = $this->get_all_updates();
return isset( $updates[ $plugin_id ] ) && is_object( $updates[ $plugin_id ] ) ? $updates[ $plugin_id ] : false;
@@ -21548,7 +21612,14 @@
false,
false,
false,
- $premium_license->secret_key
+ $premium_license->secret_key,
+ false,
+ false,
+ false,
+ null,
+ array(),
+ true,
+ $premium_license->user_id
);
return;
@@ -21600,6 +21671,8 @@
return;
}
+ $this->do_action( 'after_license_activation' );
+
$premium_license = new FS_Plugin_License( $license );
// Updated site plan.
@@ -21679,6 +21752,8 @@
'error'
);
+ $this->do_action( 'after_license_deactivation', $license );
+
return;
}
@@ -21699,6 +21774,8 @@
$this->_store_account();
+ $this->do_action( 'after_license_deactivation', $license );
+
if ( $show_notice ) {
$this->_admin_notices->add(
sprintf( $this->is_only_premium() ?
@@ -22060,6 +22137,7 @@
* @param int $expiration Since 1.2.2.7
* @param bool|string $newer_than Since 2.2.1
* @param bool|string $fetch_readme Since 2.2.1
+ * @param bool $fetch_upgrade_notice Since 2.12.1
*
* @return object|false Plugin latest tag info.
*/
@@ -22068,7 +22146,8 @@
$flush = true,
$expiration = WP_FS__TIME_24_HOURS_IN_SEC,
$newer_than = false,
- $fetch_readme = true
+ $fetch_readme = true,
+ $fetch_upgrade_notice = false
) {
$this->_logger->entrance();
@@ -22141,6 +22220,10 @@
$expiration = null;
}
+ if ( true === $fetch_upgrade_notice ) {
+ $latest_version_endpoint = add_query_arg( 'include_upgrade_notice', 'true', $latest_version_endpoint );
+ }
+
$tag = $this->get_api_site_or_plugin_scope()->get(
$latest_version_endpoint,
$flush,
@@ -22286,20 +22369,20 @@
* was initiated by the admin.
* @param bool|number $plugin_id
* @param bool $flush Since 1.1.7.3
- * @param int $expiration Since 1.2.2.7
- * @param bool|string $newer_than Since 2.2.1
*/
- private function check_updates(
- $background = false,
- $plugin_id = false,
- $flush = true,
- $expiration = FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
- $newer_than = false
- ) {
+ private function check_updates( $background = false, $plugin_id = false, $flush = true ) {
$this->_logger->entrance();
+ $newer_than = ( $this->is_premium() ? $this->get_plugin_version() : false );
+
// Check if there's a newer version for download.
- $new_version = $this->_fetch_newer_version( $plugin_id, $flush, $expiration, $newer_than );
+ $new_version = $this->_fetch_newer_version(
+ $plugin_id,
+ $flush,
+ FS_Plugin_Updater::UPDATES_CHECK_CACHE_EXPIRATION,
+ $newer_than,
+ ( false !== $newer_than )
+ );
$update = null;
if ( is_object( $new_version ) ) {
@@ -23444,7 +23527,7 @@
$params['plugin_public_key'] = $this->get_public_key();
}
- $result = $api->get( 'pricing.json?' . http_build_query( $params ) );
+ $result = $api->get( $this->add_show_pending( 'pricing.json?' . http_build_query( $params ) ) );
break;
case 'start_trial':
$trial_plan_id = fs_request_get( 'plan_id' );
@@ -24625,23 +24708,39 @@
$this->get_premium_slug() :
$this->premium_plugin_basename();
- return sprintf(
- /* translators: %1$s: Product title; %2$s: Plan title */
- $this->get_text_inline( ' The paid version of %1$s is already installed. Please activate it to start benefiting the %2$s features. %3$s', 'activate-premium-version' ),
- sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
- $plan_title,
- sprintf(
- '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
- ( $this->is_theme() ?
- wp_nonce_url( 'themes.php?action=activate&stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
- wp_nonce_url( 'plugins.php?action=activate&plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
- esc_html( sprintf(
- /* translators: %s: Plan title */
- $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
- $plan_title
- ) )
- )
- );
+ if ( is_admin() ) {
+ return sprintf(
+ /* translators: %1$s: Product title; %2$s: Plan title */
+ $this->get_text_inline( ' The paid version of %1$s is already installed. Please activate it to start benefiting from the %2$s features. %3$s', 'activate-premium-version' ),
+ sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
+ $plan_title,
+ sprintf(
+ '<a style="margin-left: 10px;" href="%s"><button class="button button-primary">%s</button></a>',
+ ( $this->is_theme() ?
+ wp_nonce_url( 'themes.php?action=activate&stylesheet=' . $premium_theme_slug_or_plugin_basename, 'switch-theme_' . $premium_theme_slug_or_plugin_basename ) :
+ wp_nonce_url( 'plugins.php?action=activate&plugin=' . $premium_theme_slug_or_plugin_basename, 'activate-plugin_' . $premium_theme_slug_or_plugin_basename ) ),
+ esc_html( sprintf(
+ /* translators: %s: Plan title */
+ $this->get_text_inline( 'Activate %s features', 'activate-x-features' ),
+ $plan_title
+ ) )
+ )
+ );
+ } else {
+ return sprintf(
+ /* translators: %1$s: Product title; %3$s: Plan title */
+ $this->get_text_inline( ' The paid version of %1$s is already installed. Please navigate to the %2$s to activate it and start benefiting from the %3$s features.', 'activate-premium-version-plugins-page' ),
+ sprintf( '<em>%s</em>', esc_html( $this->get_plugin_title() ) ),
+ sprintf(
+ '<a href="%s">%s</a>',
+ admin_url( $this->is_theme() ? 'themes.php' : 'plugins.php' ),
+ ( $this->is_theme() ?
+ $this->get_text_inline( 'Themes page', 'themes-page' ) :
+ $this->get_text_inline( 'Plugins page', 'plugins-page' ) )
+ ),
+ $plan_title
+ );
+ }
} else {
// @since 1.2.1.5 The free version is auto deactivated.
$deactivation_step = version_compare( $this->version, '1.2.1.5', '<' ) ?
--- a/post-snippets/freemius/includes/class-fs-garbage-collector.php
+++ b/post-snippets/freemius/includes/class-fs-garbage-collector.php
@@ -1,439 +1,439 @@
-<?php
- /**
- * @package Freemius
- * @copyright Copyright (c) 2015, Freemius, Inc.
- * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
- * @since 2.6.0
- */
-
- if ( ! defined( 'ABSPATH' ) ) {
- exit;
- }
-
- interface FS_I_Garbage_Collector {
- function clean();
- }
-
- class FS_Product_Garbage_Collector implements FS_I_Garbage_Collector {
- /**
- * @var FS_Options
- */
- private $_accounts;
-
- /**
- * @var string[]
- */
- private $_options_names;
-
- /**
- * @var string
- */
- private $_type;
-
- /**
- * @var string
- */
- private $_plural_type;
-
- /**
- * @var array<string, int> Map of product slugs to their last load timestamp, only for products that are not active.
- */
- private $_gc_timestamp;
-
- /**
- * @var array<string, array<string, mixed>> Map of product slugs to their data, as stored by the primary storage of `Freemius` class.
- */
- private $_storage_data;
-
- function __construct( FS_Options $_accounts, $option_names, $type ) {
- $this->_accounts = $_accounts;
- $this->_options_names = $option_names;
- $this->_type = $type;
- $this->_plural_type = ( $type . 's' );
- }
-
- function clean() {
- $this->_gc_timestamp = $this->_accounts->get_option( 'gc_timestamp', array() );
- $this->_storage_data = $this->_accounts->get_option( $this->_type . '_data', array() );
-
- $options = $this->load_options();
- $has_updated_option = false;
-
- $filtered_products = $this->get_filtered_products();
- $products_to_clean = $filtered_products['products_to_clean'];
- $active_products_by_id_map = $filtered_products['active_products_by_id_map'];
-
- foreach( $products_to_clean as $product ) {
- $slug = $product->slug;
-
- // Clear the product's data.
- foreach( $options as $option_name => $option ) {
- $updated = false;
-
- /**
- * We expect to deal with only array like options here.
- * @todo - Refactor this to create dedicated GC classes for every option, then we can make the code mode predictable.
- * For example, depending on data integrity of `plugins` we can still miss something entirely in the `plugin_data` or vice-versa.
- * A better algorithm is to iterate over all options individually in separate classes and check against primary storage to see if those can be garbage collected.
- * But given the chance of data integrity issue is very low, we let this run for now and gather feedback.
- */
- if ( ! is_array( $option ) ) {
- continue;
- }
-
- if ( array_key_exists( $slug, $option ) ) {
- unset( $option[ $slug ] );
- $updated = true;
- } else if ( array_key_exists( "{$slug}:{$this->_type}", $option ) ) { /* admin_notices */
- unset( $option[ "{$slug}:{$this->_type}" ] );
- $updated = true;
- } else if ( isset( $product->id ) && array_key_exists( $product->id, $option ) ) { /* all_licenses, add-ons, and id_slug_type_path_map */
- $is_inactive_by_id = ! isset( $active_products_by_id_map[ $product->id ] );
- $is_inactive_by_slug = (
- 'id_slug_type_path_map' === $option_name &&
- (
- ! isset( $option[ $product->id ]['slug'] ) ||
- $slug === $option[ $product->id ]['slug']
- )
- );
-
- if ( $is_inactive_by_id || $is_inactive_by_slug ) {
- unset( $option[ $product->id ] );
- $updated = true;
- }
- } else if ( /* file_slug_map */
- isset( $product->file ) &&
- array_key_exists( $product->file, $option ) &&
- $slug === $option[ $product->file ]
- ) {
- unset( $option[ $product->file ] );
- $updated = true;
- }
-
- if ( $updated ) {
- $this->_accounts->set_option( $option_name, $option );
-
- $options[ $option_name ] = $option;
-
- $has_updated_option = true;
- }
- }
-
- // Clear the product's data from the primary storage.
- if ( isset( $this->_storage_data[ $slug ] ) ) {
- unset( $this->_storage_data[ $slug ] );
- $has_updated_option = true;
- }
-
- // Clear from GC timestamp.
- // @todo - This perhaps needs a separate garbage collector for all expired products. But the chance of left-over is very slim.
- if ( isset( $this->_gc_timestamp[ $slug ] ) ) {
- unset( $this->_gc_timestamp[ $slug ] );
- $has_updated_option = true;
- }
- }
-
- $this->_accounts->set_option( 'gc_timestamp', $this->_gc_timestamp );
- $this->_accounts->set_option( $this->_type . '_data', $this->_storage_data );
-
- return $has_updated_option;
- }
-
- private function get_all_option_names() {
- return array_merge(
- array(
- 'admin_notices',
- 'updates',
- 'all_licenses',
- 'addons',
- 'id_slug_type_path_map',
- 'file_slug_map',
- ),
- $this->_options_names
- );
- }
-
- private function get_products() {
- $products = $this->_accounts->get_option( $this->_plural_type, array() );
-
- // Fill any missing product found in the primary storage.
- // @todo - This wouldn't be needed if we use dedicated GC design for every options. The options themselves would provide such information.
- foreach( $this->_storage_data as $slug => $product_data ) {
- if ( ! isset( $products[ $slug ] ) ) {
- $products[ $slug ] = (object) $product_data;
- }
-
- // This is needed to handle a scenario in which there are duplicate sets of data for the same product, but one of them needs to be removed.
- $products[ $slug ] = clone $products[ $slug ];
-
- // The reason for having the line above. This also handles a scenario in which the slug is either empty or not empty but incorrect.
- $products[ $slug ]->slug = $slug;
- }
-
- $this->update_gc_timestamp( $products );
-
- return $products;
- }
-
- private function get_filtered_products() {
- $products_to_clean = array();
- $active_products_by_id_map = array();
-
- $products = $this->get_products();
-
- foreach ( $products as $slug => $product_data ) {
- if ( ! is_object( $product_data ) ) {
- continue;
- }
-
- if ( $this->is_product_active( $slug ) ) {
- $active_products_by_id_map[ $product_data->id ] = true;
- continue;
- }
-
- $is_addon = ( ! empty( $product_data->parent_plugin_id ) );
-
- if ( ! $is_addon ) {
- $products_to_clean[] = $product_data;
- } else {
- /**
- * If add-on, add to the beginning of the array so that add-ons are removed before their parent. This is to prevent an unexpected issue when an add-on exists but its parent was already removed.
- */
- array_unshift( $products_to_clean, $product_data );
- }
- }
-
- return array(
- 'products_to_clean' => $products_to_clean,
- 'active_products_by_id_map' => $active_products_by_id_map,
- );
- }
-
- /**
- * @param string $slug
- *
- * @return bool
- */
- private function is_product_active( $slug ) {
- $instances = Freemius::_get_all_instances();
-
- foreach ( $instances as $instance ) {
- if ( $instance->get_slug() === $slug ) {
- return true;
- }
- }
-
- $expiration_time = fs_get_optional_constant( 'WP_FS__GARBAGE_COLLECTOR_EXPIRATION_TIME_SECS', ( WP_FS__TIME_WEEK_IN_SEC * 4 ) );
-
- if ( $this->get_last_load_timestamp( $slug ) > ( time() - $expiration_time ) ) {
- // Last activation was within the last 4 weeks.
- return true;
- }
-
- return false;
- }
-
- private function load_options() {
- $options = array();
- $option_names = $this->get_all_option_names();
-
- foreach ( $option_names as $option_name ) {
- $options[ $option_name ] = $this->_accounts->get_option( $option_name, array() );
- }
-
- return $options;
- }
-
- /**
- * Updates the garbage collector timestamp, only if it was not already set by the product's primary storage.
- *
- * @param array $products
- *
- * @return void
- */
- private function update_gc_timestamp( $products ) {
- foreach ($products as $slug => $product_data) {
- if ( ! is_object( $product_data ) && ! is_array( $product_data ) ) {
- continue;
- }
-
-
- // If the product is active, we don't need to update the gc_timestamp.
- if ( isset( $this->_storage_data[ $slug ]['last_load_timestamp'] ) ) {
- continue;
- }
-
- // First try to check if the product is present in the primary storage. If so update that.
- if ( isset( $this->_storage_data[ $slug ] ) ) {
- $this->_storage_data[ $slug ]['last_load_timestamp'] = time();
- } else if ( ! isset( $this->_gc_timestamp[ $slug ] ) ) {
- // If not, fallback to the gc_timestamp, but we don't want to update it more than once.
- $this->_gc_timestamp[ $slug ] = time();
- }
- }
- }
-
- private function get_last_load_timestamp( $slug ) {
- if ( isset( $this->_storage_data[ $slug ]['last_load_timestamp'] ) ) {
- return $this->_storage_data[ $slug ]['last_load_timestamp'];
- }
-
- return isset( $this->_gc_timestamp[ $slug ] ) ?
- $this->_gc_timestamp[ $slug ] :
- // This should never happen, but if it does, let's assume the product is not expired.
- time();
- }
- }
-
- class FS_User_Garbage_Collector implements FS_I_Garbage_Collector {
- private $_accounts;
-
- private $_types;
-
- function __construct( FS_Options $_accounts, array $types ) {
- $this->_accounts = $_accounts;
- $this->_types = $types;
- }
-
- function clean() {
- $users = Freemius::get_all_users();
-
- $user_has_install_map = $this->get_user_has_install_map();
-
- if ( count( $users ) === count( $user_has_install_map ) ) {
- return false;
- }
-
- $products_user_id_license_ids_map = $this->_accounts->get_option( 'user_id_license_ids_map', array() );
-
- $has_updated_option = false;
-
- foreach ( $users as $user_id => $user ) {
- if ( ! isset( $user_has_install_map[ $user_id ] ) ) {
- unset( $users[ $user_id ] );
-
- foreach( $products_user_id_license_ids_map as $product_id => $user_id_license_ids_map ) {
- unset( $user_id_license_ids_map[ $user_id ] );
-
- if ( empty( $user_id_license_ids_map ) ) {
- unset( $products_user_id_license_ids_map[ $product_id ] );
- } else {
- $products_user_id_license_ids_map[ $product_id ] = $user_id_license_ids_map;
- }
- }
-
- $this->_accounts->set_option( 'users', $users );
- $this->_accounts->set_option( 'user_id_license_ids_map', $products_user_id_license_ids_map );
-
- $has_updated_option = true;
- }
- }
-
- return $has_updated_option;
- }
-
- private function get_user_has_install_map() {
- $user_has_install_map = array();
-
- foreach ( $this->_types as $product_type ) {
- $option_name = ( WP_FS__MODULE_TYPE_PLUGIN !== $product_type ) ?
- "{$product_type}_sites" :
- 'sites';
-
- $installs = $this->_accounts->get_option( $option_name, array() );
-
- foreach ( $installs as $install ) {
- $user_has_install_map[ $install->user_id ] = true;
- }
- }
-
- return $user_has_install_map;
- }
- }
-
- // Main entry-level class.
- class FS_Garbage_Collector implements FS_I_Garbage_Collector {
- /**
- * @var FS_Garbage_Collector
- * @since 2.6.0
- */
- private static $_instance;
-
- /**
- * @return FS_Garbage_Collector
- */
- static function instance() {
- if ( ! isset( self::$_instance ) ) {
- self::$_instance = new self();
- }
-
- return self::$_instance;
- }
-
- #endregion
-
- private function __construct() {
- }
-
- function clean() {
- $_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
-
- $products_cleaners = $this->get_product_cleaners( $_accounts );
-
- $has_cleaned = false;
-
- foreach ( $products_cleaners as $products_cleaner ) {
- if ( $products_cleaner->clean() ) {
- $has_cleaned = true;
- }
- }
-
- if ( $has_cleaned ) {
- $user_cleaner = new FS_User_Garbage_Collector(
- $_accounts,
- array_keys( $products_cleaners )
- );
-
- $user_cleaner->clean();
- }
-
- // @todo - We need a garbage collector for `all_plugins` and `active_plugins` (and variants of themes).
-
- // Always store regardless of whether there were cleaned products or not since during the process, the logic may set the last load timestamp of some products.
- $_accounts->store();
- }
-
- /**
- * @param FS_Options $_accounts
- *
- * @return FS_I_Garbage_Collector[]
- */
- private function get_product_cleaners( FS_Options $_accounts ) {
- /**
- * @var FS_I_Garbage_Collector[] $products_cleaners
- */
- $products_cleaners = array();
-
- $products_cleaners[ WP_FS__MODULE_TYPE_PLUGIN ] = new FS_Product_Garbage_Collector(
- $_accounts,
- array(
- 'sites',
- 'plans',
- 'plugins',
- ),
- WP_FS__MODULE_TYPE_PLUGIN
- );
-
- $products_cleaners[ WP_FS__MODULE_TYPE_THEME ] = new FS_Product_Garbage_Collector(
- $_accounts,
- array(
- 'theme_sites',
- 'theme_plans',
- 'themes',
- ),
- WP_FS__MODULE_TYPE_THEME
- );
-
- return $products_cleaners;
- }
+<?php
+ /**
+ * @package Freemius
+ * @copyright Copyright (c) 2015, Freemius, Inc.
+ * @license https://www.gnu.org/licenses/gpl-3.0.html GNU General Public License Version 3
+ * @since 2.6.0
+ */
+
+ if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+ }
+
+ interface FS_I_Garbage_Collector {
+ function clean();
+ }
+
+ class FS_Product_Garbage_Collector implements FS_I_Garbage_Collector {
+ /**
+ * @var FS_Options
+ */
+ private $_accounts;
+
+ /**
+ * @var string[]
+ */
+ private $_options_names;
+
+ /**
+ * @var string
+ */
+ private $_type;
+
+ /**
+ * @var string
+ */
+ private $_plural_type;
+
+ /**
+ * @var array<string, int> Map of product slugs to their last load timestamp, only for products that are not active.
+ */
+ private $_gc_timestamp;
+
+ /**
+ * @var array<string, array<string, mixed>> Map of product slugs to their data, as stored by the primary storage of `Freemius` class.
+ */
+ private $_storage_data;
+
+ function __construct( FS_Options $_accounts, $option_names, $type ) {
+ $this->_accounts = $_accounts;
+ $this->_options_names = $option_names;
+ $this->_type = $type;
+ $this->_plural_type = ( $type . 's' );
+ }
+
+ function clean() {
+ $this->_gc_timestamp = $this->_accounts->get_option( 'gc_timestamp', array() );
+ $this->_storage_data = $this->_accounts->get_option( $this->_type . '_data', array() );
+
+ $options = $this->load_options();
+ $has_updated_option = false;
+
+ $filtered_products = $this->get_filtered_products();
+ $products_to_clean = $filtered_products['products_to_clean'];
+ $active_products_by_id_map = $filtered_products['active_products_by_id_map'];
+
+ foreach( $products_to_clean as $product ) {
+ $slug = $product->slug;
+
+ // Clear the product's data.
+ foreach( $options as $option_name => $option ) {
+ $updated = false;
+
+ /**
+ * We expect to deal with only array like options here.
+ * @todo - Refactor this to create dedicated GC classes for every option, then we can make the code mode predictable.
+ * For example, depending on data integrity of `plugins` we can still miss something entirely in the `plugin_data` or vice-versa.
+ * A better algorithm is to iterate over all options individually in separate classes and check against primary storage to see if those can be garbage collected.
+ * But given the chance of data integrity issue is very low, we let this run for now and gather feedback.
+ */
+ if ( ! is_array( $option ) ) {
+ continue;
+ }
+
+ if ( array_key_exists( $slug, $option ) ) {
+ unset( $option[ $slug ] );
+ $updated = true;
+ } else if ( array_key_exists( "{$slug}:{$this->_type}", $option ) ) { /* admin_notices */
+ unset( $option[ "{$slug}:{$this->_type}" ] );
+ $updated = true;
+ } else if ( isset( $product->id ) && array_key_exists( $product->id, $option ) ) { /* all_licenses, add-ons, and id_slug_type_path_map */
+ $is_inactive_by_id = ! isset( $active_products_by_id_map[ $product->id ] );
+ $is_inactive_by_slug = (
+ 'id_slug_type_path_map' === $option_name &&
+ (
+ ! isset( $option[ $product->id ]['slug'] ) ||
+ $slug === $option[ $product->id ]['slug']
+ )
+ );
+
+ if ( $is_inactive_by_id || $is_inactive_by_slug ) {
+ unset( $option[ $product->id ] );
+ $updated = true;
+ }
+ } else if ( /* file_slug_map */
+ isset( $product->file ) &&
+ array_key_exists( $product->file, $option ) &&
+ $slug === $option[ $product->file ]
+ ) {
+ unset( $option[ $product->file ] );
+ $updated = true;
+ }
+
+ if ( $updated ) {
+ $this->_accounts->set_option( $option_name, $option );
+
+ $options[ $option_name ] = $option;
+
+ $has_updated_option = true;
+ }
+ }
+
+ // Clear the product's data from the primary storage.
+ if ( isset( $this->_storage_data[ $slug ] ) ) {
+ unset( $this->_storage_data[ $slug ] );
+ $has_updated_option = true;
+ }
+
+ // Clear from GC timestamp.
+ // @todo - This perhaps needs a separate garbage collector for all expired products. But the chance of left-over is very slim.
+ if ( isset( $this->_gc_timestamp[ $slug ] ) ) {
+ unset( $this->_gc_timestamp[ $slug ] );
+ $has_updated_option = true;
+ }
+ }
+
+ $this->_accounts->set_option( 'gc_timestamp', $this->_gc_timestamp );
+ $this->_accounts->set_option( $this->_type . '_data', $this->_storage_data );
+
+ return $has_updated_option;
+ }
+
+ private function get_all_option_names() {
+ return array_merge(
+ array(
+ 'admin_notices',
+ 'updates',
+ 'all_licenses',
+ 'addons',
+ 'id_slug_type_path_map',
+ 'file_slug_map',
+ ),
+ $this->_options_names
+ );
+ }
+
+ private function get_products() {
+ $products = $this->_accounts->get_option( $this->_plural_type, array() );
+
+ // Fill any missing product found in the primary storage.
+ // @todo - This wouldn't be needed if we use dedicated GC design for every options. The options themselves would provide such information.
+ foreach( $this->_storage_data as $slug => $product_data ) {
+ if ( ! isset( $products[ $slug ] ) ) {
+ $products[ $slug ] = (object) $product_data;
+ }
+
+ // This is needed to handle a scenario in which there are duplicate sets of data for the same product, but one of them needs to be removed.
+ $products[ $slug ] = clone $products[ $slug ];
+
+ // The reason for having the line above. This also handles a scenario in which the slug is either empty or not empty but incorrect.
+ $products[ $slug ]->slug = $slug;
+ }
+
+ $this->update_gc_timestamp( $products );
+
+ return $products;
+ }
+
+ private function get_filtered_products() {
+ $products_to_clean = array();
+ $active_products_by_id_map = array();
+
+ $products = $this->get_products();
+
+ foreach ( $products as $slug => $product_data ) {
+ if ( ! is_object( $product_data ) ) {
+ continue;
+ }
+
+ if ( $this->is_product_active( $slug ) ) {
+ $active_products_by_id_map[ $product_data->id ] = true;
+ continue;
+ }
+
+ $is_addon = ( ! empty( $product_data->parent_plugin_id ) );
+
+ if ( ! $is_addon ) {
+ $products_to_clean[] = $product_data;
+ } else {
+ /**
+ * If add-on, add to the beginning of the array so that add-ons are removed before their parent. This is to prevent an unexpected issue when an add-on exists but its parent was already removed.
+ */
+ array_unshift( $products_to_clean, $product_data );
+ }
+ }
+
+ return array(
+ 'products_to_clean' => $products_to_clean,
+ 'active_products_by_id_map' => $active_products_by_id_map,
+ );
+ }
+
+ /**
+ * @param string $slug
+ *
+ * @return bool
+ */
+ private function is_product_active( $slug ) {
+ $instances = Freemius::_get_all_instances();
+
+ foreach ( $instances as $instance ) {
+ if ( $instance->get_slug() === $slug ) {
+ return true;
+ }
+ }
+
+ $expiration_time = fs_get_optional_constant( 'WP_FS__GARBAGE_COLLECTOR_EXPIRATION_TIME_SECS', ( WP_FS__TIME_WEEK_IN_SEC * 4 ) );
+
+ if ( $this->get_last_load_timestamp( $slug ) > ( time() - $expiration_time ) ) {
+ // Last activation was within the last 4 weeks.
+ return true;
+ }
+
+ return false;
+ }
+
+ private function load_options() {
+ $options = array();
+ $option_names = $this->get_all_option_names();
+
+ foreach ( $option_names as $option_name ) {
+ $options[ $option_name ] = $this->_accounts->get_option( $option_name, array() );
+ }
+
+ return $options;
+ }
+
+ /**
+ * Updates the garbage collector timestamp, only if it was not already set by the product's primary storage.
+ *
+ * @param array $products
+ *
+ * @return void
+ */
+ private function update_gc_timestamp( $products ) {
+ foreach ($products as $slug => $product_data) {
+ if ( ! is_object( $product_data ) && ! is_array( $product_data ) ) {
+ continue;
+ }
+
+
+ // If the product is active, we don't need to update the gc_timestamp.
+ if ( isset( $this->_storage_data[ $slug ]['last_load_timestamp'] ) ) {
+ continue;
+ }
+
+ // First try to check if the product is present in the primary storage. If so update that.
+ if ( isset( $this->_storage_data[ $slug ] ) ) {
+ $this->_storage_data[ $slug ]['last_load_timestamp'] = time();
+ } else if ( ! isset( $this->_gc_timestamp[ $slug ] ) ) {
+ // If not, fallback to the gc_timestamp, but we don't want to update it more than once.
+ $this->_gc_timestamp[ $slug ] = time();
+ }
+ }
+ }
+
+ private function get_last_load_timestamp( $slug ) {
+ if ( isset( $this->_storage_data[ $slug ]['last_load_timestamp'] ) ) {
+ return $this->_storage_data[ $slug ]['last_load_timestamp'];
+ }
+
+ return isset( $this->_gc_timestamp[ $slug ] ) ?
+ $this->_gc_timestamp[ $slug ] :
+ // This should never happen, but if it does, let's assume the product is not expired.
+ time();
+ }
+ }
+
+ class FS_User_Garbage_Collector implements FS_I_Garbage_Collector {
+ private $_accounts;
+
+ private $_types;
+
+ function __construct( FS_Options $_accounts, array $types ) {
+ $this->_accounts = $_accounts;
+ $this->_types = $types;
+ }
+
+ function clean() {
+ $users = Freemius::get_all_users();
+
+ $user_has_install_map = $this->get_user_has_install_map();
+
+ if ( count( $users ) === count( $user_has_install_map ) ) {
+ return false;
+ }
+
+ $products_user_id_license_ids_map = $this->_accounts->get_option( 'user_id_license_ids_map', array() );
+
+ $has_updated_option = false;
+
+ foreach ( $users as $user_id => $user ) {
+ if ( ! isset( $user_has_install_map[ $user_id ] ) ) {
+ unset( $users[ $user_id ] );
+
+ foreach( $products_user_id_license_ids_map as $product_id => $user_id_license_ids_map ) {
+ unset( $user_id_license_ids_map[ $user_id ] );
+
+ if ( empty( $user_id_license_ids_map ) ) {
+ unset( $products_user_id_license_ids_map[ $product_id ] );
+ } else {
+ $products_user_id_license_ids_map[ $product_id ] = $user_id_license_ids_map;
+ }
+ }
+
+ $this->_accounts->set_option( 'users', $users );
+ $this->_accounts->set_option( 'user_id_license_ids_map', $products_user_id_license_ids_map );
+
+ $has_updated_option = true;
+ }
+ }
+
+ return $has_updated_option;
+ }
+
+ private function get_user_has_install_map() {
+ $user_has_install_map = array();
+
+ foreach ( $this->_types as $product_type ) {
+ $option_name = ( WP_FS__MODULE_TYPE_PLUGIN !== $product_type ) ?
+ "{$product_type}_sites" :
+ 'sites';
+
+ $installs = $this->_accounts->get_option( $option_name, array() );
+
+ foreach ( $installs as $install ) {
+ $user_has_install_map[ $install->user_id ] = true;
+ }
+ }
+
+ return $user_has_install_map;
+ }
+ }
+
+ // Main entry-level class.
+ class FS_Garbage_Collector implements FS_I_Garbage_Collector {
+ /**
+ * @var FS_Garbage_Collector
+ * @since 2.6.0
+ */
+ private static $_instance;
+
+ /**
+ * @return FS_Garbage_Collector
+ */
+ static function instance()