: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
$menu_slug = $this->_menu->get_slug();
if ( $page === $menu_slug ) {
// e.g., {$menu_slug}-account, {$menu_slug}-affiliation, etc.
* Plugin's account page + sync license URL.
* @author Vova Feldman (@svovaf)
* @param bool|number $plugin_id
* @param bool $add_action_nonce
function _get_sync_license_url( $plugin_id = false, $add_action_nonce = true, $params = array() ) {
if ( is_numeric( $plugin_id ) ) {
$params['plugin_id'] = $plugin_id;
return $this->get_account_url(
$this->get_unique_affix() . '_sync_license',
* @author Vova Feldman (@svovaf)
* @param bool|string $action
* @param bool $add_action_nonce
function get_account_url( $action = false, $params = array(), $add_action_nonce = true ) {
if ( is_string( $action ) ) {
$params['fs_action'] = $action;
self::require_pluggable_essentials();
return ( $add_action_nonce && is_string( $action ) ) ?
fs_nonce_url( $this->_get_admin_page_url( 'account', $params ), $action ) :
$this->_get_admin_page_url( 'account', $params );
* @author Vova Feldman (@svovaf)
* @param bool $add_action_nonce
* @uses get_account_url()
function get_account_tab_url( $tab, $action = false, $params = array(), $add_action_nonce = true ) {
return $this->get_account_url( $action, $params, $add_action_nonce );
* @author Vova Feldman (@svovaf)
* @param bool|string $topic
* @param bool|string $message
* @param bool|string $summary Since 2.5.1.
function contact_url( $topic = false, $message = false, $summary = false ) {
if ( is_string( $topic ) ) {
$params['topic'] = $topic;
if ( is_string( $message ) ) {
$params['message'] = $message;
if ( is_string( $summary ) ) {
$params['summary'] = $summary;
if ( $this->is_addon() ) {
$params['addon_id'] = $this->get_id();
return $this->get_parent_instance()->_get_admin_page_url( 'contact', $params );
return $this->_get_admin_page_url( 'contact', $params );
* Add-on direct info URL.
* @author Vova Feldman (@svovaf)
function addon_url( $slug ) {
return $this->_get_admin_page_url( 'addons', array(
* @author Vova Feldman (@svovaf)
function get_addons_url() {
return $this->_get_admin_page_url( 'addons' );
------------------------------------------------------------------------------------------------------------------*/
* @param bool $prefix_slug
function get_logger( $id = '', $prefix_slug = true ) {
return FS_Logger::get_logger( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id );
* Note: This method is used externally so don't delete it.
* @param bool $load_options
* @param bool $prefix_slug
* @return FS_Option_Manager
function get_options_manager( $id, $load_options = false, $prefix_slug = true ) {
return FS_Option_Manager::get_manager( ( $prefix_slug ? $this->_slug : '' ) . ( ( ! $prefix_slug || empty( $id ) ) ? '' : '_' ) . $id, $load_options );
------------------------------------------------------------------------------------------------------------------*/
private static function _encrypt( $str ) {
* The encrypt/decrypt functions are used to protect
* the user from messing up with some of the sensitive
* data stored for the module as a JSON in the database.
* I used the same suggested hack by the theme review team.
* For more details, look at the function `Base64UrlDecode()`
* in `./sdk/FreemiusBase.php`.
* @todo Remove this hack once the base64 error is removed from the Theme Check.
* @author Vova Feldman (@svovaf)
$fn = 'base64' . '_encode';
static function _decrypt( $str ) {
* The encrypt/decrypt functions are used to protect
* the user from messing up with some of the sensitive
* data stored for the module as a JSON in the database.
* I used the same suggested hack by the theme review team.
* For more details, look at the function `Base64UrlDecode()`
* in `./sdk/FreemiusBase.php`.
* @todo Remove this hack once the base64 error is removed from the Theme Check.
* @author Vova Feldman (@svovaf)
$fn = 'base64' . '_decode';
* @author Vova Feldman (@svovaf)
* @param FS_Entity $entity
* @return FS_Entity Return an encrypted clone entity.
private static function _encrypt_entity( FS_Entity $entity ) {
$props = get_object_vars( $entity );
foreach ( $props as $key => $val ) {
$clone->{$key} = self::_encrypt( $val );
* @author Vova Feldman (@svovaf)
* @param FS_Entity $entity
* @return FS_Entity Return an decrypted clone entity.
private static function decrypt_entity( FS_Entity $entity ) {
$props = get_object_vars( $entity );
foreach ( $props as $key => $val ) {
$clone->{$key} = self::_decrypt( $val );
* @author Vova Feldman (@svovaf)
public static function _get_user_by_email( $email ) {
self::$_static_logger->entrance();
$email = trim( strtolower( $email ) );
$users = self::get_all_users();
if ( is_array( $users ) ) {
foreach ( $users as $user ) {
if ( $email === trim( strtolower( $user->email ) ) ) {
#----------------------------------------------------------------------------------
#region Account (Loading, Updates & Activation)
#----------------------------------------------------------------------------------
* Load account information (user + site).
* @author Vova Feldman (@svovaf)
private function _load_account() {
$this->_logger->entrance();
$this->do_action( 'before_account_load' );
$users = self::get_all_users();
$plans = self::get_all_plans( $this->_module_type );
if ( $this->_logger->is_on() && is_admin() ) {
$this->_logger->log( 'users = ' . var_export( $users, true ) );
$this->_logger->log( 'plans = ' . var_export( $plans, true ) );
$site = fs_is_network_admin() ?
$this->get_network_install() :
$this->get_install_by_blog_id();
if ( fs_is_network_admin() &&
$this->is_network_active() &&
FS_Site::is_valid_id( $this->_storage->network_install_blog_id )
$first_install = $this->find_first_install();
if ( is_null( $first_install ) ) {
unset( $this->_storage->network_install_blog_id );
$site = $first_install['install'];
$this->_storage->network_install_blog_id = $first_install['blog_id'];
if ( is_object( $site ) &&
is_numeric( $site->id ) &&
is_numeric( $site->user_id ) &&
FS_Plugin_Plan::is_valid_id( $site->plan_id )
if ( fs_is_network_admin() && $this->_is_network_active ) {
$user = $this->get_network_user();
if ( is_object( $user ) ) {
$this->_user = clone $user;
} else if ( $this->_site ) {
$user = self::_get_user_by_id( $this->_site->user_id );
if ( ! is_object( $user ) && FS_User::is_valid_id( $this->_storage->prev_user_id ) ) {
* Try to load the previous owner. This recovery is used for the following use-case:
* 2. Cloning site1 to site2
* 3. Ownership switch in site1 (same applies for site2)
* 4. Install data sync on site2
* 5. Now site2's install is associated with the new owner which does not exists locally.
$user = self::_get_user_by_id( $this->_storage->prev_user_id );
if ( ! is_object( $user ) ) {
* This is a special fault tolerance mechanism to handle a scenario that the user data is missing.
! isset( $this->_storage->user_recovery_from_install_last_attempt_timestamp ) ||
time() > ( $this->_storage->user_recovery_from_install_last_attempt_timestamp + FS_Clone_Manager::CLONE_RESOLUTION_MAX_EXECUTION_TIME )
$user = $this->sync_user_by_current_install();
if ( is_object( $user ) ) {
$this->_storage->user_was_recovered_from_install = true;
$this->_storage->user_recovery_from_install_attempts = isset( $this->_storage->user_recovery_from_install_attempts ) ?
( $this->_storage->user_recovery_from_install_attempts + 1 ) :
if ( $this->_storage->user_recovery_from_install_attempts >= 3 ) {
$this->delete_current_install( false );
$this->_storage->user_recovery_from_install_last_attempt_timestamp = time();
$this->_user = ( $user instanceof FS_User ) ?
if ( is_object( $this->_user ) ) {
$this->_licenses = $this->get_user_licenses( $this->_user->id );
if ( is_object( $this->_site ) ) {
$this->_plans = isset( $plans[ $this->_slug ] ) ?
if ( ! is_array( $this->_plans ) || empty( $this->_plans ) ) {
for ( $i = 0, $len = count( $this->_plans ); $i < $len; $i ++ ) {
if ( $this->_plans[ $i ] instanceof FS_Plugin_Plan ) {
$this->_plans[ $i ] = self::decrypt_entity( $this->_plans[ $i ] );
unset( $this->_plans[ $i ] );
$this->_license = $this->_get_license_by_id( $this->_site->license_id );
if ( $this->_site->version != $this->get_plugin_version() ) {
// If stored install version is different than current installed plugin version,
// then update plugin version event.
$this->update_plugin_version_event();
if ( true === $this->_storage->require_license_activation &&
! fs_request_get_bool( 'require_license', true )
$this->_storage->require_license_activation = false;
if ( $this->is_theme() ) {
$this->_register_account_hooks();
if ( $this->is_user_in_admin() && $this->is_clone() ) {
if ( empty( FS_Clone_Manager::instance()->get_clone_identification_timestamp() ) ) {
FS_Clone_Manager::instance()->store_clone_identification_timestamp();
FS_Clone_Manager::instance()->maybe_update_clone_resolution_support_flag( $this->_storage->sdk_last_version );
$this->send_pending_clone_update_once();
* Special user recovery mechanism.
* @author Vova Feldman (@svovaf)
* @param number|null $site_user_id
private function sync_user_by_current_install( $site_user_id = null ) {
$site_user_id = FS_Site::is_valid_id( $site_user_id ) ?
$api = $this->get_api_site_scope();
$uid = $this->get_anonymous_id();
$request_path = "/users/{$site_user_id}.json?uid={$uid}";
$result = $api->get( $request_path, false, WP_FS__TIME_10_MIN_IN_SEC );
if ( $this->is_api_result_entity( $result ) ) {
$user = new FS_User( $result );
$error_code = FS_Api::get_error_code( $result );
if ( in_array( $error_code, array( 'invalid_unique_id', 'user_cannot_be_recovered' ) ) ) {
* Those API errors will continue coming and are not recoverable with the
* current site's data. Therefore, extend the API call's cached result to 7 days.
$api->update_cache_expiration( $request_path, WP_FS__TIME_WEEK_IN_SEC );
* @author Vova Feldman (@svovaf)
* @param bool|array $plans
private function _set_account( FS_User $user, FS_Site $site, $plans = false ) {
$site->user_id = $user->id;
if ( false !== $plans ) {