: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
$install_data = $this->get_install_diff_for_api( $install_data, $install, $override );
$common_diff = $this->get_install_diff_for_api( $common, $install, $override );
$is_common_diff = ! empty( $common_diff );
foreach ( $common_diff as $k => $v ) {
if ( ! isset( $common_diff_union[ $k ] ) ) {
$common_diff_union[ $k ] = $v;
$is_common_diff_for_any_site = $is_common_diff_for_any_site || $is_common_diff;
if ( ! empty( $install_data ) || $is_common_diff || $is_keepalive ) {
// Add install ID and site unique ID.
$install_data['id'] = $install->id;
$install_data['uid'] = $uid;
$install_data['url'] = $url;
$subsite_data_for_api_by_install_id[ $install->id ] = $install_data;
$install_url_by_install_id[ $install->id ] = $install->url;
$subsite_registration_date_by_install_id[ $install->id ] = $registration_date;
$installs_data = array_merge(
array_values( $subsite_data_for_api_by_install_id )
if ( 0 < count( $installs_data ) && ( $is_common_diff_for_any_site || ! $only_diff ) ) {
$installs_data[] = $common;
} else if ( ! empty( $common_diff_union ) ) {
$installs_data[] = $common_diff_union;
foreach ( $installs_data as &$data ) {
* Compare site actual data to the stored install data and return the differences for an API data sync.
* @author Vova Feldman (@svovaf)
* @param FS_Site $install
* @param string[] string $override
private function get_install_diff_for_api( $site, $install, $override = array() ) {
$special_override = false;
foreach ( $site as $p => $v ) {
if ( property_exists( $install, $p ) ) {
if ( ( is_bool( $install->{$p} ) || ! empty( $install->{$p} ) ) &&
$val = self::get_api_sanitized_property( $p, $v );
if ( $install->{$p} != $val ) {
if ( isset( $override[ $p ] ) ||
$special_override = true;
if ( $special_override || 0 < count( $diff ) ) {
// Add special params only if has at least one
// standard param, or if explicitly requested to
// override a special param or a param which is not exist
// in the install object.
$diff = array_merge( $diff, $special );
* @author Leo Fajardo (@leorw)
private function send_pending_clone_update_once() {
$this->_logger->entrance();
if ( ! empty( $this->_storage->clone_id ) ) {
$install_clone = $this->get_api_site_scope()->call(
array( 'site_url' => self::get_unfiltered_site_url() )
if ( $this->is_api_result_entity( $install_clone ) ) {
$this->_storage->clone_id = $install_clone->id;
* @author Leo Fajardo (@leorw)
* @param string $resolution_type
* @param FS_Site $clone_context_install
function send_clone_resolution_update( $resolution_type, $clone_context_install ) {
$this->_logger->entrance();
if ( empty( $this->_storage->clone_id ) ) {
* If the current site is now different from the context install before the clone resolution, we need to override `$this->_site` so that the API call below will be made with the right install scope entity.
if ( $clone_context_install->id != $this->_site->id ) {
$new_install_id = $this->_site->id;
$current_site = $this->_site;
$this->_site = $clone_context_install;
$this->get_api_site_scope( $flush )->call(
"/clones/{$this->_storage->clone_id}",
'resolution' => $resolution_type,
'new_install_id' => $new_install_id,
if ( is_object( $current_site ) ) {
* Ensure that the install scope entity is updated back to the previous install entity.
$this->_site = $current_site;
// Restore the previous install scope entity of the API.
$this->get_api_site_scope( true );
* Update install only if changed.
* @author Vova Feldman (@svovaf)
* @param string[] string $override
* @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared.
* @return false|object|string
private function send_install_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
$this->_logger->entrance();
$check_properties = $this->get_install_data_for_api( $override );
$params = $check_properties;
$params = $this->get_install_diff_for_api( $check_properties, $this->_site, $override );
if ( empty( $params ) ) {
$keepalive_only_update = $this->should_send_keepalive_update();
if ( ! $keepalive_only_update ) {
* There are no updates to send including keepalive.
* @author Leo Fajardo (@leorw)
if ( $is_two_way_sync ) {
* Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
* @author Leo Fajardo (@leorw)
if ( ! is_multisite() ) {
// Update last install sync timestamp.
$this->set_cron_execution_timestamp( 'install_sync' );
$params['uid'] = $this->get_anonymous_id();
$this->set_keepalive_timestamp();
// Send updated values to FS.
$site = $this->api_site_call( '/', 'put', $params, true );
if ( $is_two_way_sync && $this->is_api_result_entity( $site ) ) {
* Clear scheduled install sync after a two-way sync call.
* @author Leo Fajardo (@leorw)
if ( ! is_multisite() ) {
// I successfully sent install update, clear scheduled sync if exist.
$this->clear_install_sync_cron();
* Update installs only if changed.
* @author Vova Feldman (@svovaf)
* @param string[] string $override
* @param bool $is_two_way_sync @since 2.5.0 If true and there's a successful API request, the install sync cron will be cleared.
* @return false|object|string
private function send_installs_update( $override = array(), $flush = false, $is_two_way_sync = false ) {
$this->_logger->entrance();
* Pass `true` to use the network level storage since the update is for many installs.
* @author Leo Fajardo (@leorw)
$should_send_keepalive = $this->should_send_keepalive_update( true );
$installs_data = $this->get_installs_data_for_api( $override, ! $flush, $should_send_keepalive );
if ( empty( $installs_data ) ) {
if ( $is_two_way_sync ) {
// Update last install sync timestamp during a two-way sync call as we expect that updates are sent during this call.
$this->set_cron_execution_timestamp( 'install_sync' );
* Pass `true` to use the network level storage since the update is for many installs.
* @author Leo Fajardo (@leorw)
$this->set_keepalive_timestamp( true );
// Send updated values to FS.
$result = $this->get_api_user_scope()->call( "/plugins/{$this->_plugin->id}/installs.json", 'put', $installs_data );
if ( $is_two_way_sync && $this->is_api_result_object( $result, 'installs' ) ) {
// I successfully sent a two-way installs update, clear the scheduled install sync if it exists.
$this->clear_install_sync_cron();
* @author Leo Fajardo (@leorw)
* @param bool|null $use_network_level_storage
private function should_send_keepalive_update( $use_network_level_storage = null ) {
$keepalive_timestamp = $this->_storage->get( 'keepalive_timestamp', 0, $use_network_level_storage );
if ( $keepalive_timestamp < ( time() - WP_FS__TIME_WEEK_IN_SEC ) ) {
// If updated more than 7 days ago, trigger a keepalive and update the time it was triggered.
// If updated 7 days ago or less, "flip a coin", if the value is 7 trigger a keepalive and update the last time it was triggered.
return ( 7 == rand( 1, 7 ) );
* Syncs the install owner's data if needed (i.e., if the install owner is different from the loaded user).
* @author Leo Fajardo (@leorw)
private function maybe_sync_install_user() {
if ( $this->_user->id == $this->_site->user_id ) {
// Fetch user data and store if found.
$this->sync_user_by_current_install();
* Update install only if changed.
* @author Vova Feldman (@svovaf)
* @param string[] string $override
function sync_install( $override = array(), $flush = false ) {
$this->_logger->entrance();
$site = $this->send_install_update( $override, $flush, true );
if ( ! $this->is_api_result_entity( $site ) ) {
// Failed to sync, don't update locally.
$this->_site = new FS_Site( $site );
$this->_store_site( true );
* Update install only if changed.
* @author Vova Feldman (@svovaf)
* @param string[] string $override
private function sync_installs( $override = array(), $flush = false ) {
$this->_logger->entrance();
$result = $this->send_installs_update( $override, $flush, true );
if ( false === $result ) {
if ( ! $this->is_api_result_object( $result, 'installs' ) ) {
// Failed to sync, don't update locally.
$address_to_blog_map = $this->get_address_to_blog_map();
foreach ( $result->installs as $install ) {
$this->_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 );
* Track install's custom event.
* Custom event tracking is currently only supported for specific clients.
* If you are not one of them, please don't use this method. If you will,
* the API will simply ignore your request based on the plugin ID.
* Need custom tracking for your plugin or theme?
* If you are interested in custom event tracking please contact yo@freemius.com
* @author Vova Feldman (@svovaf)
* @param string $name Event name.
* @param array $properties Associative key/value array with primitive values only
* @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
* @param bool $once If true, event will be tracked only once. IMPORTANT: Still trigger the API call.
* @return object|false Event data or FALSE on failure.
* @throws \Freemius_InvalidArgumentException
public function track_event( $name, $properties = array(), $process_at = false, $once = false ) {
$this->_logger->entrance( http_build_query( array( 'name' => $name, 'once' => $once ) ) );
if ( ! $this->is_registered() ) {
$event = array( 'type' => $name );
if ( is_numeric( $process_at ) && $process_at > time() ) {
$event['process_at'] = $process_at;
if ( ! empty( $properties ) ) {
// Verify associative array values are primitive.
foreach ( $properties as $k => $v ) {
if ( ! is_scalar( $v ) ) {
throw new Freemius_InvalidArgumentException( 'The $properties argument must be an associative key/value array with primitive values only.' );
$event['properties'] = $properties;
$result = $this->get_api_site_scope()->call( 'events.json', 'post', $event );
return $this->is_api_error( $result ) ?
* Track install's custom event only once, but it still triggers the API call.
* Custom event tracking is currently only supported for specific clients.
* If you are not one of them, please don't use this method. If you will,
* the API will simply ignore your request based on the plugin ID.
* Need custom tracking for your plugin or theme?
* If you are interested in custom event tracking please contact yo@freemius.com
* @author Vova Feldman (@svovaf)
* @param string $name Event name.
* @param array $properties Associative key/value array with primitive values only
* @param bool $process_at A valid future date-time in the following format Y-m-d H:i:s.
* @return object|false Event data or FALSE on failure.
* @throws \Freemius_InvalidArgumentException
* @user Freemius::track_event()
public function track_event_once( $name, $properties = array(), $process_at = false ) {
return $this->track_event( $name, $properties, $process_at, true );
* @author Vova Feldman (@svovaf)
* @param bool $check_user Enforce checking if user have plugins activation privileges.
function _uninstall_plugin_event( $check_user = true ) {