: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
add_action( 'deactivate_blog', array( &$this, '_after_site_deactivated_callback' ) );
add_action( 'archive_blog', array( &$this, '_after_site_deactivated_callback' ) );
add_action( 'make_spam_blog', array( &$this, '_after_site_deactivated_callback' ) );
if ( version_compare( $GLOBALS['wp_version'], '5.1', '<' ) ) {
add_action( 'deleted_blog', array( $this, '_after_site_deleted_callback' ), 10, 2 );
add_action( 'wp_delete_site', array( $this, '_after_wpsite_deleted_callback' ) );
add_action( 'activate_blog', array( &$this, '_after_site_reactivated_callback' ) );
add_action( 'unarchive_blog', array( &$this, '_after_site_reactivated_callback' ) );
add_action( 'make_ham_blog', array( &$this, '_after_site_reactivated_callback' ) );
if ( $this->is_theme() &&
$this->apply_filters( 'show_customizer_upsell', true )
// Register customizer upsell.
add_action( 'customize_register', array( &$this, '_customizer_register' ) );
add_action( 'admin_init', array( &$this, '_redirect_on_clicked_menu_link' ), WP_FS__LOWEST_PRIORITY );
if ( $this->is_theme() && ! $this->is_migration() ) {
add_action( 'admin_init', array( &$this, '_add_tracking_links' ) );
add_action( 'admin_init', array( &$this, '_add_license_activation' ) );
add_action( 'admin_init', array( &$this, '_add_premium_version_upgrade_selection' ) );
add_action( 'admin_init', array( &$this, '_add_beta_mode_update_handler' ) );
add_action( 'admin_init', array( &$this, '_add_user_change_option' ) );
add_action( 'admin_init', array( &$this, '_add_email_address_update_option' ) );
$this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
$this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
$this->add_ajax_action( 'set_data_debug_mode', array( &$this, '_set_data_debug_mode' ) );
$this->add_ajax_action( 'toggle_whitelabel_mode', array( &$this, '_toggle_whitelabel_mode_ajax_handler' ) );
if ( $this->_is_network_active && fs_is_network_admin() ) {
$this->add_ajax_action( 'network_activate', array( &$this, '_network_activate_ajax_action' ) );
$this->add_ajax_action( 'install_premium_version', array(
'_install_premium_version_ajax_action'
$this->add_ajax_action( 'submit_affiliate_application', array( &$this, '_submit_affiliate_application' ) );
$this->add_action( 'after_plans_sync', array( &$this, '_check_for_trial_plans' ) );
$this->add_action( 'sdk_version_update', array( &$this, '_sdk_version_update' ), WP_FS__DEFAULT_PRIORITY, 2 );
array( &$this, '_after_version_update' ),
$this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
add_action( 'admin_init', array( &$this, '_add_trial_notice' ) ); // @phpstan-ignore-line
add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) ); // @phpstan-ignore-line
add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
* Handle request to reset anonymous mode for `get_reconnect_url()` or reset the pending activation mode.
* @author Vova Feldman (@svovaf)
fs_request_is_action( 'reset_anonymous_mode' ) ||
fs_request_is_action( 'reset_pending_activation_mode' )
$this->get_unique_affix() === fs_request_get_raw( 'fs_unique_affix' )
add_action( 'admin_init', array( &$this, 'connect_again' ) );
* Register the required hooks right after the settings parse is completed.
* @author Vova Feldman (@svovaf)
private function register_after_settings_parse_hooks() {
! $this->has_active_valid_license()
'delete_theme_update_data',
array( &$this, '_delete_theme_update_data_action' )
if ( $this->show_settings_with_tabs() ) {
* Include the required hooks to capture the theme settings' page tabs
* @author Vova Feldman (@svovaf)
if ( ! $this->_cache->has_valid( 'tabs' ) ) {
add_action( 'admin_footer', array( &$this, '_tabs_capture' ) );
// Add license activation AJAX callback.
$this->add_ajax_action( 'store_tabs', array( &$this, '_store_tabs_ajax_action' ) );
add_action( 'admin_enqueue_scripts', array( &$this, '_store_tabs_styles' ), 9999999 );
array( &$this, '_add_freemius_tabs' ),
* The tabs JS code must be executed after the tabs capture logic (_tabs_capture()).
* That's why the priority is 11 while the tabs capture logic is added
* @author Vova Feldman (@svovaf)
if ( ! self::is_ajax() ) {
if ( ! $this->is_addon() || $this->is_only_premium() ) {
( $this->_is_network_active && fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu',
array( &$this, '_prepare_admin_menu' ),
* Makes Freemius-related updates unavailable on the "Add Plugins" admin page (/plugin-install.php) so that
* they won't interfere with the .org plugins' functionalities on that page (e.g. updating of a .org plugin).
* @author Leo Fajardo (@leorw)
* @param string|null $transient
static function _remove_fs_updates_from_plugin_install_page( $updates, $transient = null ) {
if ( is_object( $updates ) && isset( $updates->response ) ) {
foreach ( $updates->response as $file => $plugin ) {
if ( isset( $plugin->package ) && false !== strpos( $plugin->package, 'api.freemius' ) ) {
unset( $updates->response[ $file ] );
* Prepends the `fs_allow_updater_and_dialog` param to the plugin information URLs to tell the SDK to handle
* the information that is shown on the plugin details dialog that is shown when the relevant link is clicked.
* @author Leo Fajardo (@leorw)
static function _prepend_fs_allow_updater_and_dialog_flag_url_param() {
$slug_basename_map = array();
foreach ( self::$_instances as $instance ) {
if ( ! $instance->is_plugin() ) {
$slug_basename_map[ $instance->get_slug() ] = $instance->premium_plugin_basename();
<script type="text/javascript">
var slugBasenameMap = <?php echo json_encode( $slug_basename_map ) ?>;
for ( var slug in slugBasenameMap ) {
var basename = slugBasenameMap[ slug ];
// Try to get the plugin rows if on the "Plugins" page.
var $pluginRows = $( '.wp-list-table.plugins tr[data-plugin="' + basename + '"]');
if ( 0 === $pluginRows.length ) {
// Try to get the plugin rows if on the "Updates" page.
var $pluginCheckbox = $( '#update-plugins-table input[type="checkbox"][value="' + basename + '"]' );
if ( 0 !== $pluginCheckbox.length ) {
$pluginRows = $pluginCheckbox.parents( 'tr:first' );
if ( 0 === $pluginRows.length ) {
// Find the "View details" links and add the `fs_allow_updater_and_dialog` param to the URL.
$pluginRows.find( 'a[href*="plugin-install.php?tab=plugin-information"]' ).each(function() {
href = $this.attr( 'href' ).replace( '?tab=', '?fs_allow_updater_and_dialog=true&tab=');
$this.attr( 'href', href );
* @author Leo Fajardo (@leorw)
static function _maybe_add_beta_label_styles() {
$has_any_beta_version = false;
foreach ( self::$_instances as $instance ) {
if ( $instance->is_beta() ) {
$has_any_beta_version = true;
if ( $has_any_beta_version ) {
fs_enqueue_local_style( 'fs_plugins', '/admin/plugins.css' );
* @author Leo Fajardo (@leorw)
static function _maybe_add_beta_label_to_plugins_and_handle_confirmation() {
foreach ( self::$_instances as $instance ) {
if ( ! $instance->is_premium() ) {
* If there's an available beta version update, a confirmation message will be shown when the
* "Update now" link on the "Plugins" or "Themes" page is clicked.
$has_beta_update = $instance->has_beta_update();
// The "Beta" label is added separately for themes.
$instance->is_plugin() &&
if ( ! $is_beta && ! $has_beta_update ) {
$beta_data[ $instance->get_plugin_basename() ] = array( 'is_installed_version_beta' => $is_beta );
if ( ! $has_beta_update ) {
$beta_data[ $instance->get_plugin_basename() ]['beta_version_update_confirmation_message'] = sprintf(
'An update to a Beta version will replace your installed version of %s with the latest Beta release - use with caution, and not on production sites. You have been warned.',
'beta-version-update-caution',
$instance->get_plugin_title()
fs_esc_attr_inline( 'Would you like to proceed with the update?', 'update-confirmation', $instance->get_slug() )
if ( empty( $beta_data ) ) {
<script type="text/javascript">
var betaData = <?php echo json_encode( $beta_data ) ?>;
for ( var pluginBasename in betaData ) {
if ( ! betaData.hasOwnProperty( pluginBasename ) ) {
if ( ! betaData[ pluginBasename ].is_installed_version_beta ) {
var $parentContainer = $( '.wp-list-table.plugins tr[data-plugin="' + pluginBasename + '"]' );
if ( 0 === $parentContainer.length ) {
$parentContainer.find( '.plugin-title > strong:first-child').append(
'<span class="fs-tag fs-info"><?php fs_esc_js_echo_inline( 'Beta', 'beta' ) ?></span>'
// Wait a little bit before adding the event handler, otherwise, it will be overridden by the core WP logic.
$( '.plugins .update-message .update-link, .themes .theme .update-message' ).on( 'click', function() {
var $parentContainer = $( this ).parents( 'tr:first' );
pluginBasename = ( 0 !== $parentContainer.length ) ?
$parentContainer.data( 'plugin' ) :
$( this ).parents( '.theme:first' ).data( 'slug' );
betaData[ pluginBasename ] &&
betaData[ pluginBasename ].beta_version_update_confirmation_message &&
! confirm( betaData[ pluginBasename ].beta_version_update_confirmation_message )
* Keeping the uninstall hook registered for free or premium plugin version may result to a fatal error that
* could happen when a user tries to uninstall either version while one of them is still active. Uninstalling a
* plugin will trigger inclusion of the free or premium version and if one of them is active during the
* uninstallation, a fatal error may occur in case the plugin's class or functions are already defined.
* @author Leo Fajardo (@leorw)
private function unregister_uninstall_hook() {
$uninstallable_plugins = (array) get_option( 'uninstall_plugins' );
unset( $uninstallable_plugins[ $this->_free_plugin_basename ] );
unset( $uninstallable_plugins[ $this->premium_plugin_basename() ] );
update_option( 'uninstall_plugins', $uninstallable_plugins );
* @since 1.2.0 Invalidate module's main file cache, otherwise, FS_Plugin_Updater will not fetch updates.
* @param bool $store_prev_path
private function clear_module_main_file_cache( $store_prev_path = true ) {
if ( ! isset( $this->_storage->plugin_main_file ) ||
empty( $this->_storage->plugin_main_file->path )
if ( ! $store_prev_path ) {
* Storing the previous path is not needed when clearing the cache after an SDK version update since
* the main purpose of the cache clearing in that event is to correct a wrong plugin main file path
* which causes data mix-up between plugins (e.g. titles and versions of an add-on and its parent plugin).
* @author Leo Fajardo (@leorw)
unset( $this->_storage->plugin_main_file->path );
$plugin_main_file = clone $this->_storage->plugin_main_file;
// Store cached path (2nd layer cache).
$plugin_main_file->prev_path = $plugin_main_file->path;
unset( $plugin_main_file->path );
$this->_storage->plugin_main_file = $plugin_main_file;
* Clear global cached path.
* @author Leo Fajardo (@leorw)
$id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map' );
unset( $id_slug_type_path_map[ $this->_module_id ]['path'] );
self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
* @author Leo Fajardo (@leorw)
function _hook_action_links_and_register_account_hooks() {
if ( $this->is_migration() ) {
( self::is_plugins_page() && $this->is_plugin() ) ||
( self::is_themes_page() && $this->is_theme() ) ||
fs_request_is_action_secure( $this->get_unique_affix() . '_reconnect' )
$this->_add_tracking_links();
if ( self::is_plugins_page() && $this->is_plugin() ) {
$this->hook_plugin_action_links();
$this->_register_account_hooks();
* @author Vova Feldman (@svovaf)
private function _register_account_hooks() {
* Always show the deactivation feedback form since we added
* automatic free version deactivation upon premium code activation.
'submit_uninstall_reason',
array( &$this, '_submit_uninstall_reason_action' )
'cancel_subscription_or_trial',
array( &$this, 'cancel_subscription_or_trial_ajax_action' )
if ( ! $this->is_addon() || $this->is_parent_plugin_installed() ) {
if ( ( $this->is_plugin() && self::is_plugins_page() ) ||
( $this->is_theme() && self::is_themes_page() )
add_action( 'admin_footer', array( &$this, '_add_deactivation_feedback_dialog_box' ) );
* Leverage backtrace to find caller plugin file path.
* @param bool $is_init Is initiation sequence.
* @param string $main_file Since 2.5.0 expects the module's main file path to potentially purge the cached path.
* @author Vova Feldman (@svovaf)
private function _find_caller_plugin_file( $is_init = false, $main_file = '' ) {
// Try to load the cached value of the file path.
if ( isset( $this->_storage->plugin_main_file ) ) {
$plugin_main_file = $this->_storage->plugin_main_file;
if ( ! empty( $plugin_main_file->path ) ) {
$absolute_path = $this->get_absolute_path( $plugin_main_file->path );
if ( file_exists( $absolute_path ) ) {
if ( $is_init && $absolute_path !== $this->get_absolute_path( $main_file ) ) {
// Update cached path if not matching the actual path.
$plugin_main_file->path = $main_file;
$this->_storage->plugin_main_file = $plugin_main_file;
* `clear_module_main_file_cache()` is clearing the plugin's cached path on
* deactivation. Therefore, if any plugin/theme was initiating `Freemius`
* with that plugin's slug, it was overriding the empty plugin path with a wrong path.
* So, we've added a special mechanism with a 2nd layer of cache that uses `prev_path`
* when the class instantiator isn't the module.