: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
$this->send_install_update();
* Get a sanitized array with the WordPress version, SDK version, and PHP version.
* Each version is trimmed after the 16th char.
* @author Vova Feldman (@svovaf)
private function get_versions() {
$versions['sdk_version'] = $this->version;
// Collect these diagnostic information only if it's allowed.
if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
$versions['platform_version'] = get_bloginfo( 'version' );
$versions['programming_language_version'] = phpversion();
foreach ( $versions as $k => $version ) {
$versions[ $k ] = self::get_api_sanitized_property( $k, $version );
* Get sanitized site language.
* @param string $language
* @author Vova Feldman (@svovaf)
private static function get_sanitized_language( $language = '', $max_len = self::LANGUAGE_MAX_CHARS ) {
if ( empty( $language ) ) {
$language = get_bloginfo( 'language' );
return substr( $language, 0, $max_len );
* Get core version stripped from pre-release and build.
* @author Vova Feldman (@svovaf)
* @param bool $include_pre_release
private static function get_core_version(
$max_len = self::VERSION_MAX_CHARS,
$include_pre_release = false
if ( empty( $version ) ) {
if ( is_numeric( $version ) ) {
$is_float_version = is_float( $version );
$version = (string) $version;
* Casting a whole float number to a string cuts the decimal point. This part make sure to add the missing decimal part to the version.
if ( $is_float_version && false === strpos( $version, '.' ) ) {
if ( ! is_string( $version ) ) {
$pre_release_regex = $include_pre_release ?
'(\-(alpha|beta|RC)([0-9]+)?)?' :
if ( 0 === preg_match( '/^([0-9]+(\.[0-9]+){0,' . ( $parts - 1 ) . '}' . $pre_release_regex . ')/i', $version, $matches ) ) {
// Version is not starting with a digit.
return substr( $matches[1], 0, $max_len );
*@author Vova Feldman (@svovaf)
private static function get_api_sanitized_property( $prop, $val ) {
if ( ! is_string( $val ) || empty( $val ) ) {
case 'programming_language_version':
// Get core PHP version, which can have up to 3 parts (ignore pre-releases).
return self::get_core_version( $val );
// Get the exact WordPress version, which can have up to 3 parts (including pre-releases).
return self::get_core_version( $val, 3, self::VERSION_MAX_CHARS, true );
// Get the exact SDK version, which can have up to 4 parts.
return self::get_core_version( $val, 4 );
// Get the entire version but just limited in length.
return substr( $val, 0, self::VERSION_MAX_CHARS );
return self::get_sanitized_language( $val );
* @author Leo Fajardo (@leorw)
function has_beta_update() {
! empty( $this->_storage->beta_data ) &&
( true === $this->_storage->beta_data['is_beta'] ) &&
version_compare( $this->_storage->beta_data['version'], $this->get_plugin_version(), '>' )
* @author Leo Fajardo (@leorw)
! empty( $this->_storage->beta_data ) &&
( true === $this->_storage->beta_data['is_beta'] ) &&
( $this->get_plugin_version() === $this->_storage->beta_data['version'] )
* @author Vova Feldman (@svovaf)
* @param array $override_with
* @param bool|int|null $network_level_or_blog_id If true, return params for network level opt-in. If integer, get params for specified blog in the network.
function get_opt_in_params( $override_with = array(), $network_level_or_blog_id = null ) {
$this->_logger->entrance();
$current_user = self::_get_current_wp_user();
$activation_action = $this->get_unique_affix() . '_activate_new';
$return_url = $this->is_anonymous() ?
// If skipped already, then return to the account page.
$this->get_account_url( $activation_action, array(), false ) :
// Return to the module's main page.
$this->get_after_activation_url( 'after_connect_url', array( 'fs_action' => $activation_action ) );
$versions = $this->get_versions();
$params = array_merge( $versions, array(
'user_firstname' => $current_user->user_firstname,
'user_lastname' => $current_user->user_lastname,
'user_email' => $current_user->user_email,
'plugin_slug' => $this->_slug,
'plugin_id' => $this->get_id(),
'plugin_public_key' => $this->get_public_key(),
'plugin_version' => $this->get_plugin_version(),
'return_url' => fs_nonce_url( $return_url, $activation_action ),
'account_url' => fs_nonce_url( $this->_get_admin_page_url(
array( 'fs_action' => 'sync_user' )
'is_premium' => $this->is_premium(),
'is_uninstalled' => false,
'is_localhost' => WP_FS__IS_LOCALHOST,
if ( $this->is_addon() ) {
$parent_fs = $this->get_parent_instance();
$params['parent_plugin_slug'] = $parent_fs->_slug;
$params['parent_plugin_id'] = $parent_fs->get_id();
if ( true === $network_level_or_blog_id ) {
if ( ! isset( $override_with['sites'] ) ) {
$params['sites'] = $this->get_sites_for_network_level_optin();
$site = is_numeric( $network_level_or_blog_id ) ?
array( 'blog_id' => $network_level_or_blog_id ) :
$site = $this->get_site_info( $site );
$diagnostic_info = array();
if ( FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed() ) {
$diagnostic_info = array(
'site_name' => $site['title'],
'language' => self::get_sanitized_language( $site['language'] ),
$params = array_merge( $params, $diagnostic_info, array(
'site_uid' => $site['uid'],
'site_url' => $site['url'],
if ( $this->is_pending_activation() &&
! empty( $this->_storage->pending_license_key )
$params['license_key'] = $this->_storage->pending_license_key;
if ( WP_FS__SKIP_EMAIL_ACTIVATION && $this->has_secret_key() ) {
// Even though rand() is known for its security issues,
// the timestamp adds another layer of protection.
// It would be very hard for an attacker to get the secret key form here.
// Plus, this should never run in production since the secret should never
// be included in the production version.
$params['ts'] = WP_FS__SCRIPT_START_TIME;
$params['salt'] = md5( uniqid( rand() ) );
if ( is_multisite() && function_exists( 'get_network' ) ) {
$params['network_uid'] = $this->get_anonymous_network_id();
return array_merge( $params, $override_with );
* 1. If successful opt-in or pending activation returns the next page that the user should be redirected to.
* 2. If there was an API error, return the API result.
* @author Vova Feldman (@svovaf)
* @param string|bool $email
* @param string|bool $first
* @param string|bool $last
* @param string|bool $license_key
* @param bool $is_uninstall If "true", this means that the module is currently being uninstalled.
* In this case, the user and site info will be sent to the server but no
* data will be saved to the WP installation's database.
* @param number|bool $trial_plan_id
* @param bool $is_disconnected Whether to opt in without tracking.
* @param null|bool $is_marketing_allowed
* @param array $sites If network-level opt-in, an array of containing details of sites.
$is_disconnected = false,
$is_marketing_allowed = null,
$this->_logger->entrance();
if ( false === $email ) {
$current_user = self::_get_current_wp_user();
$email = $current_user->user_email;
* @since 1.2.1 If activating with license key, ignore the context-user
* since the user will be automatically loaded from the license.
if ( empty( $license_key ) ) {
// Clean up pending license if opt-ing in again.
$this->_storage->remove( 'pending_license_key' );
$fs_user = Freemius::_get_user_by_email( $email );
if ( is_object( $fs_user ) && ! $this->is_pending_activation() ) {
return $this->install_with_user(
if ( ! empty( $email ) ) {
$user_info['user_email'] = $email;
if ( ! empty( $first ) ) {
$user_info['user_firstname'] = $first;
if ( ! empty( $last ) ) {
$user_info['user_lastname'] = $last;
if ( ! empty( $sites ) ) {
$user_info['sites'] = $sites;
$params = $this->get_opt_in_params( $user_info, $is_network );
$filtered_license_key = false;
if ( is_string( $license_key ) ) {
$filtered_license_key = $this->apply_filters( 'license_key', $license_key );
$params['license_key'] = $filtered_license_key;
} else if ( FS_Plugin_Plan::is_valid_id( $trial_plan_id ) ) {
$params['trial_plan_id'] = $trial_plan_id;
$params['uninstall_params'] = array(
'reason_id' => $this->_storage->uninstall_reason->id,
'reason_info' => $this->_storage->uninstall_reason->info
if ( isset( $params['license_key'] ) ) {
$fs_user = Freemius::_get_user_by_email( $email );
if ( is_object( $fs_user ) ) {
* If opting in with a context license and the context WP Admin user already opted in
* before from the current site, add the user context security params to avoid the
* unnecessary email activation when the context license is owned by the same context user.
* @author Leo Fajardo (@leorw)
$params = array_merge( $params, FS_Security::instance()->get_context_params(
'install_with_existing_user'
if ( is_bool( $is_marketing_allowed ) ) {
$params['is_marketing_allowed'] = $is_marketing_allowed;
$params['is_disconnected'] = $is_disconnected;
$params['format'] = 'json';
$params['is_extensions_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_extensions_tracking_allowed();
$params['is_diagnostic_tracking_allowed'] = FS_Permission_Manager::instance( $this )->is_diagnostic_tracking_allowed();
$url = $this->add_show_pending( WP_FS__ADDRESS . '/action/service/user/install/' );
$response = self::safe_remote_post( $url, $request );
if ( is_wp_error( $response ) ) {
* @var WP_Error $response
$result = new stdClass();
$error_code = $response->get_error_code();
$error_type = str_replace( ' ', '', ucwords( str_replace( '_', ' ', $error_code ) ) );
$result->error = (object) array(
'message' => $response->get_error_message(),
$this->maybe_modify_api_curl_error_message( $result );
if ( FS_Api::is_blocked( $result ) ) {
$result->error->message = $this->generate_api_blocked_notice_message_from_result( $result );
if ( empty( $license_key ) && $this->is_enable_anonymous() ) {
$this->skip_connection( fs_is_network_admin() );
$is_connected = ( ! FS_Api::is_blocked( $result ) );
$this->update_connectivity_info( $is_connected );
$this->update_connectivity_info( true );
// Module is being uninstalled, don't handle the returned data.
* When json_decode() executed on PHP 5.2 with an invalid JSON, it will throw a PHP warning. Unfortunately, the new Theme Check doesn't allow PHP silencing and the theme review team isn't open to change that, therefore, instead of using `@json_decode()` we had to use the method without the `@` directive.
* @author Vova Feldman (@svovaf)
* @link https://themes.trac.wordpress.org/ticket/46134#comment:5
* @link https://themes.trac.wordpress.org/ticket/46134#comment:9
* @link https://themes.trac.wordpress.org/ticket/46134#comment:12
* @link https://themes.trac.wordpress.org/ticket/46134#comment:14
$decoded = is_string( $response['body'] ) ?
json_decode( $response['body'] ) :
if ( empty( $decoded ) ) {
if ( ! $this->is_api_result_object( $decoded ) ) {
if ( ! empty( $params['license_key'] ) ) {
// Pass the fully entered license key to the failure handler.
$params['license_key'] = $license_key;
$this->apply_filters( 'after_install_failure', $decoded, $params );
} else if ( isset( $decoded->pending_activation ) && $decoded->pending_activation ) {
foreach ( $sites as $site ) {
$site_ids[] = $site['blog_id'];
* Store the sites so that they can be installed once the user has clicked on the activation link
* @author Leo Fajardo (@leorw)
$this->_storage->pending_sites_info = array(
'license_key' => $license_key,
'trial_plan_id' => $trial_plan_id