: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
if ( ! is_object( $site_active_plugins_cache ) ) {
$site_active_plugins_cache = (object) array(
if ( ! empty( $site_active_plugins_cache->timestamp ) &&
( $time - $site_active_plugins_cache->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
// Don't send plugin updates if last update was in the past 5 min.
// Write timestamp to lock the logic.
$site_active_plugins_cache->timestamp = $time;
self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
// Reload options from DB.
self::$_accounts->load( true );
$site_active_plugins_cache = self::$_accounts->get_option( $site_active_plugins_option_name );
if ( $time != $site_active_plugins_cache->timestamp ) {
// If timestamp is different, then another thread captured the lock.
* Collection of all plugins (network level).
$network_plugins_cache = self::$_accounts->get_option( $network_plugins_option_name );
if ( ! is_object( $network_plugins_cache ) ) {
$network_plugins_cache = (object) array(
// Check if there's a change in plugins.
$network_plugins = self::get_network_plugins();
$site_active_plugins = self::get_site_active_plugins();
$network_plugins_thumbprint = $this->get_plugins_thumbprint( $network_plugins );
$site_active_plugins_thumbprint = $this->get_plugins_thumbprint( $site_active_plugins );
// Check if plugins status changed (version or active/inactive).
$network_plugins_changed = ( $network_plugins_cache->md5 !== $network_plugins_thumbprint );
$site_active_plugins_changed = ( $site_active_plugins_cache->md5 !== $site_active_plugins_thumbprint );
if ( ! $network_plugins_changed &&
! $site_active_plugins_changed
$plugins_update_data = array();
foreach ( $network_plugins_cache->plugins as $basename => $data ) {
if ( ! isset( $network_plugins[ $basename ] ) ) {
$uninstalled_plugin_data = $data;
$uninstalled_plugin_data['is_active'] = false;
$uninstalled_plugin_data['is_uninstalled'] = true;
$plugins_update_data[] = $uninstalled_plugin_data;
unset( $network_plugins[ $basename ] );
unset( $network_plugins_cache->plugins[ $basename ] );
unset( $site_active_plugins_cache->plugins[ $basename ] );
$was_active = $data['is_active'] ||
( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
true === $site_active_plugins_cache->plugins[ $basename ]['is_active'] );
$is_active = $network_plugins[ $basename ]['is_active'] ||
( isset( $site_active_plugins[ $basename ] ) &&
$site_active_plugins[ $basename ]['is_active'] );
if ( ! isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
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;
} else if ( isset( $site_active_plugins_cache->plugins[ $basename ] ) &&
! isset( $site_active_plugins[ $basename ] )
// Plugin was site level deactivated.
unset( $site_active_plugins_cache->plugins[ $basename ] );
$prev_version = $data['version'];
$current_version = $network_plugins[ $basename ]['Version'];
if ( $was_active !== $is_active || $prev_version !== $current_version ) {
// Plugin activated or deactivated, or version changed.
if ( $was_active !== $is_active ) {
if ( $data['is_active'] != $network_plugins[ $basename ]['is_active'] ) {
$network_plugins_cache->plugins[ $basename ]['is_active'] = $data['is_active'];
if ( $prev_version !== $current_version ) {
$network_plugins_cache->plugins[ $basename ]['Version'] = $current_version;
$updated_plugin_data = $data;
$updated_plugin_data['is_active'] = $is_active;
$updated_plugin_data['version'] = $current_version;
$updated_plugin_data['title'] = $network_plugins[ $basename ]['Name'];
$plugins_update_data[] = $updated_plugin_data;
// Find new plugins that weren't yet seen before.
foreach ( $network_plugins as $basename => $data ) {
if ( ! isset( $network_plugins_cache->plugins[ $basename ] ) ) {
'version' => $data['Version'],
'title' => $data['Name'],
'is_active' => $data['is_active'],
'is_uninstalled' => false,
$network_plugins_cache->plugins[ $basename ] = $new_plugin;
$is_site_level_active = (
isset( $site_active_plugins[ $basename ] ) &&
$site_active_plugins[ $basename ]['is_active']
* If not network active, set the activity status based on the site-level plugin status.
if ( ! $new_plugin['is_active'] ) {
$new_plugin['is_active'] = $is_site_level_active;
$plugins_update_data[] = $new_plugin;
if ( isset( $site_active_plugins[ $basename ] ) ) {
$site_active_plugins_cache->plugins[ $basename ] = $new_plugin;
$site_active_plugins_cache->plugins[ $basename ]['is_active'] = $is_site_level_active;
$site_active_plugins_cache->md5 = $site_active_plugins_thumbprint;
$site_active_plugins_cache->timestamp = $time;
self::$_accounts->set_option( $site_active_plugins_option_name, $site_active_plugins_cache, true );
$network_plugins_cache->md5 = $network_plugins_thumbprint;
$network_plugins_cache->timestamp = $time;
self::$_accounts->set_option( $network_plugins_option_name, $network_plugins_cache, true );
return $plugins_update_data;
* Return a list of modified themes since the last sync.
* There's no point to store a themes counter since even if the number of
* themes 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_themes_data_for_api() {
$option_name = 'all_themes';
$all_cached_themes = self::$_accounts->get_option( $option_name );
if ( ! is_object( $all_cached_themes ) ) {
$all_cached_themes = (object) array(
if ( ! empty( $all_cached_themes->timestamp ) &&
( $time - $all_cached_themes->timestamp ) < WP_FS__TIME_5_MIN_IN_SEC
// Don't send theme updates if last update was in the past 5 min.
// Write timestamp to lock the logic.
$all_cached_themes->timestamp = $time;
self::$_accounts->set_option( $option_name, $all_cached_themes, true );
// Reload options from DB.
self::$_accounts->load( true );
$all_cached_themes = self::$_accounts->get_option( $option_name );
if ( $time != $all_cached_themes->timestamp ) {
// If timestamp is different, then another thread captured the lock.
$active_theme = wp_get_theme();
$active_theme_stylesheet = $active_theme->get_stylesheet();
// Check if there's a change in themes.
$all_themes = wp_get_themes();
// Check if themes changed.
foreach ( $all_themes as $slug => $data ) {
$is_active = ( $slug === $active_theme_stylesheet );
$themes_signature .= $slug . ',' .
( $is_active ? '1' : '0' ) . ';';
// Check if themes status changed (version or active/inactive).
$themes_changed = ( $all_cached_themes->md5 !== md5( $themes_signature ) );
$themes_update_data = array();
// Change in themes, report changes.
// Update existing themes info.
foreach ( $all_cached_themes->themes as $slug => $data ) {
$is_active = ( $slug === $active_theme_stylesheet );
if ( ! isset( $all_themes[ $slug ] ) ) {
$uninstalled_theme_data = $data;
$uninstalled_theme_data['is_active'] = false;
$uninstalled_theme_data['is_uninstalled'] = true;
$themes_update_data[] = $uninstalled_theme_data;
unset( $all_themes[ $slug ] );
unset( $all_cached_themes->themes[ $slug ] );
} else if ( $data['is_active'] !== $is_active ||
$data['version'] !== $all_themes[ $slug ]->version
// Plugin activated or deactivated, or version changed.
$all_cached_themes->themes[ $slug ]['is_active'] = $is_active;
$all_cached_themes->themes[ $slug ]['version'] = $all_themes[ $slug ]->version;
$themes_update_data[] = $all_cached_themes->themes[ $slug ];
// Find new themes that weren't yet seen before.
foreach ( $all_themes as $slug => $data ) {
if ( ! isset( $all_cached_themes->themes[ $slug ] ) ) {
$is_active = ( $slug === $active_theme_stylesheet );
'version' => $data->version,
'is_active' => $is_active,
'is_uninstalled' => false,
$themes_update_data[] = $new_plugin;
$all_cached_themes->themes[ $slug ] = $new_plugin;
$all_cached_themes->md5 = md5( $themes_signature );
$all_cached_themes->timestamp = time();
self::$_accounts->set_option( $option_name, $all_cached_themes, true );
return $themes_update_data;
* Get site data for API install request.
* @author Vova Feldman (@svovaf)
* @param string[] $override
* @param bool $include_plugins Since 1.1.8 by default include plugin changes.
* @param bool $include_themes Since 1.1.8 by default include plugin changes.
* @param bool $include_blog_data Since 2.3.0 by default include the current blog's data (language, title, and URL).
private function get_install_data_for_api(
$include_blog_data = true
$permissions = FS_Permission_Manager::instance( $this );
if ( $permissions->is_extensions_tracking_allowed() ) {
if ( ! defined( 'WP_FS__TRACK_PLUGINS' ) || false !== WP_FS__TRACK_PLUGINS ) {
* @since 1.1.8 Also send plugin updates.
if ( $include_plugins && ! isset( $override['plugins'] ) ) {
$plugins = $this->get_plugins_data_for_api();
if ( ! empty( $plugins ) ) {
$override['plugins'] = $plugins;
if ( ! defined( 'WP_FS__TRACK_THEMES' ) || false !== WP_FS__TRACK_THEMES ) {
* @since 1.1.8 Also send themes updates.
if ( $include_themes && ! isset( $override['themes'] ) ) {
$themes = $this->get_themes_data_for_api();
if ( ! empty( $themes ) ) {
$override['themes'] = $themes;
$versions = $this->get_versions();
if ( $include_blog_data ) {
$blog_data['url'] = self::get_unfiltered_site_url();
if ( $permissions->is_diagnostic_tracking_allowed() ) {
$blog_data = array_merge( $blog_data, array(
'language' => self::get_sanitized_language(),
'title' => get_bloginfo( 'name' ),
return array_merge( $versions, $blog_data, array(
'version' => $this->get_plugin_version(),
'is_premium' => $this->is_premium(),
'is_uninstalled' => false,
* Update installs details.
* @todo V1 of multiste network support doesn't support plugin and theme data sending.
* @author Vova Feldman (@svovaf)
* @param string[] string $override
* @param bool $is_keepalive
* @param bool $include_plugins Since 1.1.8 by default include plugin changes.
* @param bool $include_themes Since 1.1.8 by default include plugin changes.
private function get_installs_data_for_api(
* @since 1.1.8 Also send plugin updates.
// if ( $include_plugins && ! isset( $override['plugins'] ) ) {
// $plugins = $this->get_plugins_data_for_api();
// if ( ! empty( $plugins ) ) {
// $override['plugins'] = $plugins;
* @since 1.1.8 Also send themes updates.
// if ( $include_themes && ! isset( $override['themes'] ) ) {
// $themes = $this->get_themes_data_for_api();
// if ( ! empty( $themes ) ) {
// $override['themes'] = $themes;
$versions = $this->get_versions();
$common = array_merge( $versions, array(
'version' => $this->get_plugin_version(),
'is_premium' => $this->is_premium(),
$is_common_diff_for_any_site = false;
$common_diff_union = array();
$installs_data = array();
$sites = self::get_sites();
$subsite_data_for_api_by_install_id = array();
$install_url_by_install_id = array();
$subsite_registration_date_by_install_id = array();
foreach ( $sites as $site ) {
$blog_id = self::get_site_blog_id( $site );
$install = $this->get_install_by_blog_id( $blog_id );
if ( is_object( $install ) ) {
if ( $install->user_id != $this->_user->id ) {
// Install belongs to a different owner.
if ( ! $this->is_tracking_allowed( $blog_id, $install ) ) {
// Don't send updates regarding opted-out installs.
$install_data = $this->get_site_info( $site, true );
if ( FS_Clone_Manager::instance()->is_temporary_duplicate_by_blog_id( $install_data['blog_id'] ) ) {
$uid = $install_data['uid'];
$url = $install_data['url'];
$registration_date = $install_data['registration_date'];
if ( isset( $subsite_data_for_api_by_install_id[ $install->id ] ) ) {
$clone_subsite_data = $subsite_data_for_api_by_install_id[ $install->id ];
$clone_install_url = $install_url_by_install_id[ $install->id ];
$clone_subsite_registration_date = $subsite_registration_date_by_install_id[ $install->id ];
! empty( $install_data['registration_date'] ) &&
! empty( $clone_subsite_registration_date )
* If the current subsite was created after the other subsite that is also linked to the same install ID, we assume that it's a clone (not the original), and therefore, would skip its processing.
* @author Leo Fajardo (@leorw)
$skip = ( strtotime( $install_data['registration_date'] ) > strtotime( $clone_subsite_registration_date ) );
* If we already have an install with the same URL as the subsite it's stored in, skip the current subsite. Otherwise, replace the existing install's data with the current subsite's install's data if the URLs match.
* @author Leo Fajardo (@leorw)
fs_strip_url_protocol( untrailingslashit( $clone_install_url ) ) === fs_strip_url_protocol( untrailingslashit( $clone_subsite_data['url'] ) ) ||
fs_strip_url_protocol( untrailingslashit( $install->url ) ) !== fs_strip_url_protocol( untrailingslashit( $url ) )
// Store the skipped subsite's ID so that the clone resolution manager can try to resolve the clone install that is stored in that subsite later on.
FS_Clone_Manager::instance()->store_blog_install_info( $blog_id );
unset( $install_data['blog_id'] );
unset( $install_data['uid'] );
unset( $install_data['url'] );
unset( $install_data['registration_date'] );
$install_data['is_active'] = $this->is_active_for_site( $blog_id );
$install_data['is_uninstalled'] = $install->is_uninstalled;