: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
if ( false === $this->has_api_connectivity() && ! $this->is_premium() ) {
// Reset connectivity test cache.
$this->clear_connectivity_info();
$storage_keys_for_removal[] = 'connectivity_test';
if ( $is_network_deactivation ) {
if ( isset( $this->_storage->sticky_optin_added_ms ) ) {
unset( $this->_storage->sticky_optin_added_ms );
if ( ! empty( $storage_keys_for_removal ) ) {
$sites = self::get_sites();
foreach ( $sites as $site ) {
$blog_id = self::get_site_blog_id( $site );
foreach ( $storage_keys_for_removal as $key ) {
$this->_storage->remove( $key, false, $blog_id );
$this->_storage->save( $blog_id );
// Clear API cache on deactivation.
$this->remove_sdk_reference();
* @author Vova Feldman (@svovaf)
private function remove_sdk_reference() {
global $fs_active_plugins;
foreach ( $fs_active_plugins->plugins as $sdk_path => $data ) {
if ( $this->_plugin_basename == $data->plugin_path ) {
unset( $fs_active_plugins->plugins[ $sdk_path ] );
fs_fallback_to_newest_active_sdk();
* @author Vova Feldman (@svovaf)
* @param bool $is_anonymous
* @param bool|int $network_or_blog_id Since 2.0.0
private function set_anonymous_mode( $is_anonymous = true, $network_or_blog_id = 0 ) {
// Store information regarding skip to try and opt-in the user
'timestamp' => WP_FS__SCRIPT_START_TIME,
'version' => $this->get_plugin_version(),
if ( true === $network_or_blog_id ) {
$this->_storage->is_anonymous_ms = $skip_info;
$this->_storage->store( 'is_anonymous', $skip_info, $network_or_blog_id );
$this->network_upgrade_mode_completed();
// Update anonymous mode cache.
$this->_is_anonymous = $is_anonymous;
* @author Vova Feldman (@svovaf)
* @param bool|int $network_or_blog_id
private function unset_anonymous_mode( $network_or_blog_id = 0 ) {
if ( true === $network_or_blog_id ) {
unset( $this->_storage->is_anonymous_ms );
$this->_storage->remove( 'is_anonymous', true, $network_or_blog_id );
* @author Vova Feldman (@svovaf)
* @param int $blog_id Site ID.
* @param int $user_id User ID.
* @param string $domain Site domain.
* @param string $path Site path.
* @param int $network_id Network ID. Only relevant on multi-network installations.
* @param array $meta Metadata. Used to set initial site options.
* @uses Freemius::is_license_network_active() to check if the context license was network activated by the super-admin.
* @uses Freemius::is_network_connected() to check if the super-admin network opted-in.
* @uses Freemius::is_network_anonymous() to check if the super-admin network skipped.
* @uses Freemius::is_network_delegated_connection() to check if the super-admin network delegated the connection to the site admins.
public function _after_new_blog_callback( $blog_id, $user_id, $domain, $path, $network_id, $meta ) {
$this->_logger->entrance();
if ( ! $this->_is_network_active ) {
FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
if ( $this->is_premium() &&
$this->is_network_connected() &&
is_object( $this->_license ) &&
$this->_license->can_activate( FS_Site::is_localhost_by_address( $domain ) ) &&
$this->is_license_network_active( $blog_id )
* Running the premium version, the license was network activated, and the license can also be activated on the current site -> so try to opt-in with the license key.
$current_blog_id = get_current_blog_id();
$license = clone $this->_license;
$this->switch_to_blog( $blog_id );
// Opt-in with network user.
$this->install_with_user(
$this->get_network_user(),
if ( is_object( $this->_site ) ) {
if ( $this->_site->license_id == $license->id ) {
* If the license was activated successfully, sync the license data from the remote server.
$this->_license = $license;
$this->sync_site_license();
$this->switch_to_blog( $current_blog_id );
if ( is_object( $site ) ) {
FS_Clone_Manager::instance()->store_blog_install_info( $blog_id, $site );
// Already connected (with or without a license), so no need to continue.
if ( $this->is_network_anonymous() ) {
* Opt-in was network skipped so automatically skip the opt-in for the new site.
$this->skip_site_connection( $blog_id );
} else if ( $this->is_network_delegated_connection() ) {
* Opt-in was network delegated so automatically delegate the opt-in for the new site's admin.
$this->delegate_site_connection( $blog_id );
} else if ( $this->is_network_connected() ) {
* Opt-in was network activated so automatically opt-in with the network user and new site admin.
$current_blog_id = get_current_blog_id();
$this->switch_to_blog( $blog_id );
// Opt-in with network user.
$this->install_with_user(
$this->get_network_user(),
$this->switch_to_blog( $current_blog_id );
* If the super-admin mixed different options (connect, skip, delegated):
* a) If at least one site connection was delegated, then automatically delegate connection.
* b) Otherwise, it means that at least one site was skipped and at least one site was connected. For a simplified UX in the initial release of the multisite network integration, skip the connection for the newly created site. If the super-admin will want to opt-in they can still do that from the network level Account page.
$has_delegated_site = false;
$sites = self::get_sites();
foreach ( $sites as $wp_site ) {
$blog_id = self::get_site_blog_id( $wp_site );
if ( $this->is_site_delegated_connection( $blog_id ) ) {
$has_delegated_site = true;
if ( $has_delegated_site ) {
$this->delegate_site_connection( $blog_id );
$this->skip_site_connection( $blog_id );
* Store the new blog's information even if there's no install so that when a clone install is stored in the new blog's storage, we can try to resolve it automatically.
* @author Leo Fajardo (@leorw)
FS_Clone_Manager::instance()->store_blog_install_info( $new_blog_id, $site );
* @author Vova Feldman (@svovaf)
* @param \WP_Site $new_site
public function _after_wp_initialize_site_callback( WP_Site $new_site, $args ) {
$this->_logger->entrance();
$this->_after_new_blog_callback(
// Dummy user ID (not in use).
// Dummy meta, not in use.
* @author Vova Feldman (@svovaf)
* @param bool|int|int[] $network_or_blog_ids Since 2.0.0.
private function reset_anonymous_mode( $network_or_blog_ids = false ) {
if ( true === $network_or_blog_ids ) {
$this->unset_anonymous_mode( true );
if ( fs_is_network_admin() ) {
$this->_is_anonymous = null;
// Rest anonymous mode for all non-delegated sub-sites.
$blog_ids = $this->get_non_delegated_blog_ids();
if ( false === $network_or_blog_ids ) {
$network_or_blog_ids = 0;
$blog_ids = is_array( $network_or_blog_ids ) ?
array( $network_or_blog_ids );
foreach ( $blog_ids as $blog_id ) {
if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
$this->_is_anonymous = null;
foreach ( $blog_ids as $blog_id ) {
$this->unset_anonymous_mode( $blog_id );
* Ensure that this field is also "false", otherwise, if the current module's type is "theme" and the module
* has no menus, the opt-in popup will not be shown immediately (in this case, the user will have to click
* on the admin notice that contains the opt-in link in order to trigger the opt-in popup).
* @author Leo Fajardo (@leorw)
if ( ! $this->_is_network_active ) {
$this->_is_anonymous = null;
* @author Leo Fajardo (@leorw)
private function update_license_required_permissions_if_anonymous() {
if ( ! $this->is_anonymous() ) {
$this->reset_anonymous_mode( fs_is_network_admin() );
FS_Permission_Manager::instance( $this )->update_permissions_tracking_flag( array(
* This is used to ensure that before redirecting to the opt-in page after resetting the anonymous mode or
* deleting the account in the network level, the URL of the page to redirect to is correct.
* @author Leo Fajardo (@leorw)
private function maybe_set_slug_and_network_menu_exists_flag() {
if ( ! empty( $this->_dynamically_added_top_level_page_hook_name ) ) {
$this->_menu->set_slug_and_network_menu_exists_flag( $this->_menu->has_menu() ?
$this->_menu->get_slug() :
* Clears the anonymous mode and redirects to the opt-in screen.
* @author Vova Feldman (@svovaf)
function connect_again() {
if ( ! $this->is_anonymous() && ! $this->is_pending_activation() ) {
if ( $this->is_anonymous() ) {
$this->reset_anonymous_mode( fs_is_network_admin() );
$activation_url_params = array();
if ( $this->is_pending_activation() ) {
$this->clear_pending_activation_mode();
if ( fs_request_get_bool( 'require_license' ) ) {
$activation_url_params['require_license'] = true;
$this->maybe_set_slug_and_network_menu_exists_flag();
fs_redirect( $this->get_activation_url( $activation_url_params ) );
* Skip account connect, and set anonymous mode.
* @author Vova Feldman (@svovaf)
* @param bool|int|int[] $network_or_blog_ids Since 2.5.1
function skip_connection( $network_or_blog_ids = false ) {
$this->_logger->entrance();
$this->_admin_notices->remove_sticky( 'connect_account' );
if ( true === $network_or_blog_ids ) {
$this->set_anonymous_mode( true, true );
if ( fs_is_network_admin() ) {
$this->_is_anonymous = null;
// Rest anonymous mode for all non-delegated sub-sites.
$blog_ids = $this->get_non_delegated_blog_ids();
if ( false === $network_or_blog_ids ) {
$network_or_blog_ids = 0;
$blog_ids = is_array( $network_or_blog_ids ) ?
array( $network_or_blog_ids );
foreach ( $blog_ids as $blog_id ) {
if ( 0 === $blog_id || get_current_blog_id() == $blog_id ) {
$this->_is_anonymous = null;
foreach ( $blog_ids as $blog_id ) {
$this->skip_site_connection( $blog_id );
$this->network_upgrade_mode_completed();
* Skip connection for specific site in the network.
* @author Vova Feldman (@svovaf)
* @param int|null $blog_id
private function skip_site_connection( $blog_id = null ) {
$this->_logger->entrance();
$this->_admin_notices->remove_sticky( 'connect_account', $blog_id );
$this->set_anonymous_mode( true, $blog_id );
* Plugin version update hook.
* @author Vova Feldman (@svovaf)
private function update_plugin_version_event() {
$this->_logger->entrance();
if ( ! $this->is_registered() ) {
$this->schedule_install_sync();
// $this->sync_install( array(), true );
* Generate an MD5 signature of a plugins collection.
* This helper methods used to identify changes in a plugins collection.
* @author Vova Feldman (@svovaf)
* @param array [string]array $plugins
private function get_plugins_thumbprint( $plugins ) {
foreach ( $plugins as $basename => $data ) {
$thumbprint .= $data['slug'] . ',' .
( $data['is_active'] ? '1' : '0' ) . ';';
return md5( $thumbprint );
* Return a list of modified plugins since the last sync.
* There's no point to store a plugins counter since even if the number of
* plugins didn't change, we still need to check if the versions are all the
* same and the activity state is similar.
* @author Vova Feldman (@svovaf)
private function get_plugins_data_for_api() {
$site_active_plugins_option_name = 'active_plugins';
$network_plugins_option_name = 'all_plugins';
* Collection of all site level active plugins.
$site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );