: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
return FS_Plugin_License::is_valid_id( $this->_license->parent_license_id );
* Check if user is a trial or have feature enabled license.
* @author Vova Feldman (@svovaf)
function can_use_premium_code() {
return $this->is_trial() || $this->has_features_enabled_license();
* Checks if the current user can activate plugins or switch themes. Note that this method should only be used
* after the `init` action is triggered because it is using `current_user_can()` which is only functional after
* the context user is authenticated.
* @author Leo Fajardo (@leorw)
function is_user_admin() {
* Require a super-admin when network activated, running from the network level OR if
* running from the site level but not delegated the opt-in.
* @author Vova Feldman (@svovaf)
if ( $this->_is_network_active &&
( fs_is_network_admin() || ! $this->is_delegated_connection() )
return ( $this->is_plugin() && current_user_can( is_multisite() ? 'manage_options' : 'activate_plugins' ) )
|| ( $this->is_theme() && current_user_can( 'switch_themes' ) );
* @author Vova Feldman (@svovaf)
* @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by
* @param bool $is_context_single_site @since 2.0.0. This is used when syncing a license for a single install from the
* network-level "Account" page.
* @param int|null $current_blog_id @since 2.2.3. This is passed from the `execute_cron` method and used by the
* `_sync_plugin_license` method in order to switch to the previous blog when sending
* updates for a single site in case `execute_cron` has switched to a different blog.
private function _sync_license( $background = false, $is_context_single_site = false, $current_blog_id = null ) {
$this->_logger->entrance();
$plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
$is_addon_sync = ( ! $this->_plugin->is_addon() && $plugin_id != $this->get_id() );
$this->_sync_addon_license( $plugin_id, $background );
$this->_sync_plugin_license( $background, true, $is_context_single_site, $current_blog_id );
$this->do_action( 'after_account_plan_sync', $this->get_plan_name() );
* Sync plugin's add-on license.
* @author Vova Feldman (@svovaf)
* @param number $addon_id
* @param bool $background
private function _sync_addon_license( $addon_id, $background ) {
$this->_logger->entrance();
if ( $this->is_addon_activated( $addon_id ) ) {
// If already installed, use add-on sync.
$fs_addon = self::get_instance_by_id( $addon_id );
// Add-on is network activated and network integrated.
$fs_addon->is_network_active() ||
// Add-on is not network activated or not network integrated.
$fs_addon->_sync_license( $background );
// Validate add-on exists.
$addon = $this->get_addon( $addon_id );
if ( ! is_object( $addon ) ) {
// Add add-on into account add-ons.
$account_addons = $this->get_account_addons();
if ( ! is_array( $account_addons ) ) {
$account_addons = array();
$account_addons[] = $addon->id;
$account_addons = array_unique( $account_addons );
$this->_store_account_addons( $account_addons );
$licenses = $this->_fetch_licenses( $addon->id );
if ( $this->is_array_instanceof( $licenses, 'FS_Plugin_License' ) ) {
$this->_update_licenses( $licenses, $addon->id );
if ( ! $this->is_addon_installed( $addon->id ) && FS_License_Manager::has_premium_license( $licenses ) ) {
$plans_result = $this->get_api_site_or_plugin_scope()->get( $this->add_show_pending( "/addons/{$addon_id}/plans.json" ) );
if ( ! isset( $plans_result->error ) ) {
foreach ( $plans_result->plans as $plan ) {
$plans[] = new FS_Plugin_Plan( $plan );
$this->_admin_notices->add_sticky(
( FS_Plan_Manager::instance()->has_free_plan( $plans ) ?
$this->get_text_inline( 'Your %s Add-on plan was successfully upgraded.', 'addon-successfully-upgraded-message' ) :
/* translators: %s:product name, e.g. Facebook add-on was successfully... */
$this->get_text_inline( '%s Add-on was successfully purchased.', 'addon-successfully-purchased-message' ) ),
) . ' ' . $this->get_latest_download_link(
$this->get_text_inline( 'Download the latest version', 'download-latest-version' ),
'addon_plan_upgraded_' . $addon->slug,
$this->get_text_x_inline( 'Yee-haw', 'interjection expressing joy or exuberance', 'yee-haw' ) . '!'
* Sync site's plugin plan.
* @author Vova Feldman (@svovaf)
* @param bool $background Hints the method if it's a background sync. If false, it means that was initiated by the admin.
* @param bool $send_installs_update Since 2.0.0
* @param bool $is_context_single_site Since 2.0.0. This is used when sending an update for a single install and
* syncing its license from the network-level "Account" page (e.g.: after
* activating a license only for the single install).
* @param int|null $current_blog_id Since 2.2.3. This is passed from the `execute_cron` method so that it
* can be used here to switch to the previous blog in case `execute_cron`
* has switched to a different blog.
private function _sync_plugin_license(
$send_installs_update = true,
$is_context_single_site = false,
$this->_logger->entrance();
$is_site_level_sync = ( $is_context_single_site || fs_is_blog_admin() || ! $this->_is_network_active );
if ( ! $send_installs_update ) {
* @todo This line will execute install sync on a daily basis, even if running the free version (for opted-in users). The reason we want to keep it that way is for cases when the user was a paying customer, then there was a failure in subscription payment, and then after some time the payment was successful. This could be heavily optimized. For example, we can skip the $flush if the current install was never associated with a paid version.
if ( $is_site_level_sync ) {
* Switch to the previous blog since `execute_cron` may have switched to a different blog.
* @author Leo Fajardo (@leorw)
if ( is_numeric( $current_blog_id ) ) {
$this->switch_to_blog( $current_blog_id );
$result = $this->send_install_update( array(), true, true );
$is_valid = $this->is_api_result_entity( $result );
$result = $this->send_installs_update( array(), true, true );
$is_valid = $this->is_api_result_object( $result, 'installs' );
if ( $is_context_single_site ) {
// Switch back to the main blog so that the following logic will have the right entities.
$this->switch_to_blog( $this->_storage->network_install_blog_id );
// Show API message only if not background sync or if paying customer.
if ( ! $background || $this->is_paying() ) {
// Try to ping API to see if not blocked.
if ( FS_Api::is_blocked( $result ) ) {
* @author Vova Feldman (@svovaf)
* @since 1.1.6 Only show message related to one of the Freemius powered plugins. Once it will be resolved it will fix the issue for all plugins anyways. There's no point to scare users with multiple error messages.
if ( ! self::$_global_admin_notices->has_sticky( 'api_blocked' ) ) {
// Add notice immediately if not a background sync.
$add_notice = ( ! $background );
$counter = (int) get_transient( '_fs_api_connection_retry_counter' );
// We only want to add the notice after 3 consecutive failures.
$add_notice = ( 3 <= $counter );
* Update counter transient only if notice shouldn't be added. If it is added the transient will be reset anyway, because the retries mechanism should only start counting if the admin isn't aware of the connectivity issue.
* Also, since the background sync happens once a day, setting the transient expiration for a week should be enough to count 3 failures, if there's an actual connectivity issue.
set_transient( '_fs_api_connection_retry_counter', $counter + 1, WP_FS__TIME_WEEK_IN_SEC );
// Add notice instantly for not-background sync and only after 3 failed attempts for background sync.
self::$_global_admin_notices->add(
$this->generate_api_blocked_notice_message_from_result( $result ),
add_action( 'admin_footer', array( 'Freemius', '_add_api_connectivity_notice_handler_js' ) );
// Notice was just shown, reset connectivity counter.
delete_transient( '_fs_api_connection_retry_counter' );
} else if ( is_object( $result ) ) {
// Authentication params are broken.
$this->_admin_notices->add(
$this->get_text_inline( 'It seems like one of the authentication parameters is wrong. Update your Public Key, Secret Key & User ID, and try again.', 'wrong-authentication-param-message' ) . '<br> ' . $this->get_text_inline( 'Error received from the server:', 'server-error-message' ) . var_export( $result->error, true ),
// No reason to continue with license sync while there are API issues.
// API is working now. Delete the transient and start afresh.
delete_transient('_fs_api_connection_retry_counter');
if ( $is_site_level_sync ) {
$site = new FS_Site( $result );
// Map site addresses to their blog IDs.
$address_to_blog_map = $this->get_address_to_blog_map();
// Find the current context install.
foreach ( $result->installs as $install ) {
if ( $install->id == $this->_site->id ) {
$site = new FS_Site( $install );
$address = trailingslashit( fs_strip_url_protocol( $install->url ) );
$blog_id = $address_to_blog_map[ $address ];
$this->_store_site( true, $blog_id, new FS_Site( $install ) );
// Remove sticky API connectivity message.
self::$_global_admin_notices->remove_sticky( 'api_blocked' );
if ( ! $this->has_paid_plan() ) {
$this->get_network_install_blog_id()
if ( $is_context_single_site ) {
$context_blog_id = get_current_blog_id();
// Switch back to the main blog in order to properly sync the license.
$this->switch_to_blog( $this->_storage->network_install_blog_id );
* Sync licenses. Pass the site's license ID so that the foreign licenses will be fetched if the license
* associated with that ID is not included in the user's licenses collection.
( $is_context_single_site ?
if ( $is_context_single_site ) {
$this->switch_to_blog( $context_blog_id );
// Check if plan / license changed.
if ( $site->plan_id != $this->_site->plan_id ||
// Check if trial started.
$site->trial_plan_id != $this->_site->trial_plan_id ||
$site->trial_ends != $this->_site->trial_ends ||
// Check if license changed.
$site->license_id != $this->_site->license_id
if ( $site->is_trial() && ( ! $this->_site->is_trial() || $site->trial_ends != $this->_site->trial_ends ) ) {
$plan_change = 'trial_started';
// For trial with subscription use-case.
$new_license = is_null( $site->license_id ) ? null : $this->_get_license_by_id( $site->license_id );
if ( is_object( $new_license ) && $new_license->is_valid() ) {
$this->_update_site_license( $new_license );
$this->_store_licenses();
$this->_sync_site_subscription( $this->_license );
} else if ( $this->_site->is_trial() && ! $site->is_trial() && ! is_numeric( $site->license_id ) ) {
// Was in trial, but now trial expired and no license ID.
$plan_change = 'trial_expired';
$is_free = $this->is_free_plan();
// Make sure license exist and not expired.
$new_license = is_null( $site->license_id ) ?
$this->_get_license_by_id( $site->license_id );
if ( $is_free && is_null( $new_license ) && $this->has_any_license() && $this->_license->is_cancelled ) {
$this->_update_site_license( $new_license );
$this->_store_licenses();
$plan_change = 'cancelled';
} else if ( $is_free && ( ( ! is_object( $new_license ) || $new_license->is_expired() ) ) ) {
// The license is expired, so ignore upgrade method.
* The line below should be executed before trying to activate the license on the rest of the network, otherwise, the license' activation counters may be out of sync + there's no need to activate the license on the context site since it's already activated on it.
* @author Vova Feldman (@svovaf)
$this->_update_site_license( $new_license );
if ( ! $is_context_single_site &&
$this->_is_network_active &&
$new_license->quota > 1 &&
// See if license can activated on all sites.
if ( ! $this->try_activate_license_on_network( $this->_user, $new_license ) ) {
if ( ! fs_request_get_bool( 'auto_install' ) ) {
// Open the license activation dialog box on the account page.
add_action( 'admin_footer', array(
'_open_license_activation_dialog_box'
$this->_store_licenses();
$plan_change = $is_free ?
( $this->is_only_premium() ? 'activated' : 'upgraded' ) :
( is_object( $new_license ) ?
// Store updated site info.
$this->get_network_install_blog_id()
if ( ! is_object( $this->_license ) ) {
$this->maybe_update_whitelabel_flag(
FS_Plugin_License::is_valid_id( $site->license_id ) ?
$this->get_license_by_id( $site->license_id ) :
$this->maybe_update_whitelabel_flag( $this->_license );
if ( $this->_license->is_expired() ) {
if ( ! $this->has_features_enabled_license() ) {
$this->_deactivate_license();
$plan_change = 'downgraded';
$last_time_expired_license_notice_was_shown = $this->_storage->get( 'expired_license_notice_shown', 0 );
if ( time() - ( 14 * WP_FS__TIME_24_HOURS_IN_SEC ) >= $last_time_expired_license_notice_was_shown ) {
* Show the expired license notice every 14 days.
* @author Leo Fajardo (@leorw)
$plan_change = 'expired';
if ( is_numeric( $site->license_id ) && is_object( $this->_license ) ) {
$this->_sync_site_subscription( $this->_license );
if ( ! $this->is_addon() &&
$this->_site->is_beta() !== $site->is_beta()
$this->get_network_install_blog_id()
if ( $this->is_addon() || $this->has_addons() ) {
* Purge the valid user licenses cache so that when the "Account" or the "Add-Ons" page is loaded,
* an updated valid user licenses collection will be fetched from the server which is used to also
* update the account add-ons (add-ons the user has licenses for).
* @author Leo Fajardo (@leorw)
$this->purge_valid_user_licenses_cache();
$hmm_text = $this->get_text_x_inline( 'Hmm', 'something somebody says when they are thinking about what you have just said.', 'hmm' ) . '...';