: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
// Fetch prev path cache.
if ( isset( $this->_storage->plugin_main_file ) &&
! empty( $this->_storage->plugin_main_file->prev_path )
$absolute_path = $this->get_absolute_path( $this->_storage->plugin_main_file->prev_path );
if ( file_exists( $absolute_path ) ) {
$this->get_text_inline( 'Freemius SDK couldn\'t find the plugin\'s main file. Please contact sdk@freemius.com with the current error.', 'failed-finding-main-path' ) .
" Module: {$this->_slug}; SDK: " . WP_FS__SDK_VERSION . ";",
$this->get_text_inline( 'Error', 'error' ),
array( 'back_link' => true )
* Only the original instantiator that calls dynamic_init can modify the module's path.
$this->_storage->plugin_main_file = (object) array(
return $this->get_absolute_path( $main_file );
* @author Leo Fajardo (@leorw)
private function get_relative_path( $path ) {
$module_root_dir = $this->get_module_root_dir_path();
if ( 0 === strpos( $path, $module_root_dir ) ) {
$path = substr( $path, strlen( $module_root_dir ) );
* @author Leo Fajardo (@leorw)
* @param string|bool $module_type
private function get_absolute_path( $path, $module_type = false ) {
$module_root_dir = $this->get_module_root_dir_path( $module_type );
if ( 0 !== strpos( $path, $module_root_dir ) ) {
$path = fs_normalize_path( $module_root_dir . $path );
* @author Leo Fajardo (@leorw)
* @param string|bool $module_type
private function get_module_root_dir_path( $module_type = false ) {
$is_plugin = empty( $module_type ) ?
( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
return fs_normalize_path( trailingslashit( $is_plugin ?
get_theme_root( get_stylesheet() ) ) );
* @author Leo Fajardo (@leorw)
* @param number $module_id
* @return string Since 2.5.0 return the module's main file path.
private function store_id_slug_type_path_map( $module_id, $slug ) {
$id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
$id_slug_type_path_map[ $module_id ] = array(
isset( $id_slug_type_path_map[ $module_id ]['slug'] ) &&
$slug !== $id_slug_type_path_map[ $module_id ]['slug']
$id_slug_type_path_map[ $module_id ]['slug'] = $slug;
$find_caller = empty( $id_slug_type_path_map[ $module_id ]['path'] );
* This verification is for cases when suddenly the same module
* is installed but with a different folder name.
* @author Vova Feldman (@svovaf)
$find_caller = ! file_exists( $this->get_absolute_path(
$id_slug_type_path_map[ $module_id ]['path'],
$id_slug_type_path_map[ $module_id ]['type']
foreach ( $id_slug_type_path_map as $id => $data ) {
// Remove maps with empty module ID.
unset( $id_slug_type_path_map[ $id ] );
* If the module's main file path is identical to the main file path of another module then it means that the cached path of the current module or the other one with the same path is wrong, and therefore, we need to recalculate those paths.
* @author Vova Feldman (@svovaf)
if ( $id == $module_id ) {
isset( $data['path'] ) &&
$data['path'] === $id_slug_type_path_map[ $module_id ]['path']
$caller_main_file_and_type = $this->get_caller_main_file_and_type( $module_id );
$id_slug_type_path_map[ $module_id ]['type'] = $caller_main_file_and_type->module_type;
$id_slug_type_path_map[ $module_id ]['path'] = $caller_main_file_and_type->path;
self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
return $id_slug_type_path_map[ $module_id ]['path'];
* Identifies the caller type: plugin or theme.
* @author Leo Fajardo (@leorw)
* @author Vova Feldman (@svovaf)
* @since 1.2.2.3 Find the earliest module in the call stack that calls to the SDK. This fix is for cases when
* add-ons are relying on loading the SDK from the parent module, and also allows themes including the
* SDK an internal file instead of directly from functions.php.
* @since 1.2.1.7 Knows how to handle cases when an add-on includes the parent module logic.
* @param number $module_id @since 2.5.0
private function get_caller_main_file_and_type( $module_id ) {
self::require_plugin_essentials();
$all_plugins = fs_get_plugins( true );
$all_plugins_paths = array();
// Get active plugin's main files real full names (might be symlinks).
foreach ( $all_plugins as $relative_path => $data ) {
if ( false === strpos( fs_normalize_path( $relative_path ), '/' ) ) {
* Ignore plugins that don't have a folder (e.g. Hello Dolly) since they
* can't really include the SDK.
$all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
$caller_file_candidate = false;
$module_type = WP_FS__MODULE_TYPE_PLUGIN;
$themes_dir = fs_normalize_path( get_theme_root( get_stylesheet() ) );
$plugin_dir_to_skip = false;
for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
if ( empty( $bt[ $i ]['file'] ) ) {
if ( $i > 1 && ! empty( $bt[ $i - 1 ]['file'] ) && $bt[ $i ]['file'] === $bt[ $i - 1 ]['file'] ) {
// If file same as the prev file in the stack, skip it.
if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
// The string split is stupid, but otherwise, theme check
'install_and_activate_plugin',
if ( 'activate_plugin' === $bt[ $i ]['function'] ) {
* Store the directory of the activator plugin so that any other file that starts with it
* cannot be mistakenly chosen as a candidate caller file.
$caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
foreach ( $all_plugins_paths as $plugin_path ) {
$plugin_dir = fs_normalize_path( dirname( $plugin_path ) . '/' );
if ( false !== strpos( $caller_file_path, $plugin_dir ) ) {
$plugin_dir_to_skip = $plugin_dir;
// Ignore call stack hooks and files inclusion.
$caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
if ( ! empty( $plugin_dir_to_skip ) ) {
* Skip if it's an activator plugin file to avoid mistakenly choosing it as a candidate caller file.
if ( 0 === strpos( $caller_file_path, $plugin_dir_to_skip ) ) {
if ( 'functions.php' === basename( $caller_file_path ) ) {
* 1. Assumes that theme's starting execution file is functions.php.
* 2. This complex logic fixes symlink issues (e.g. with Vargant).
* @author Vova Feldman (@svovaf)
if ( $caller_file_path == fs_normalize_path( realpath( trailingslashit( $themes_dir ) . basename( dirname( $caller_file_path ) ) . '/' . basename( $caller_file_path ) ) ) ) {
$module_type = WP_FS__MODULE_TYPE_THEME;
* Relative path of the theme, e.g.:
* `my-theme/functions.php`
* @author Leo Fajardo (@leorw)
$caller_file_candidate = basename( dirname( $caller_file_path ) ) .
basename( $caller_file_path );
$caller_file_hash = md5( $caller_file_path );
if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
foreach ( $all_plugins_paths as $plugin_path ) {
if ( empty( $plugin_path ) ) {
if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
$caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
if ( isset( $caller_map[ $caller_file_hash ] ) ) {
$module_type = WP_FS__MODULE_TYPE_PLUGIN;
$caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
$caller_main_file_and_type = (object) array(
'module_type' => $module_type,
'path' => $caller_file_candidate
return apply_filters( "fs_{$module_id}_caller_main_file_and_type", $caller_main_file_and_type );
#----------------------------------------------------------------------------------
#region Deactivation Feedback Form
#----------------------------------------------------------------------------------
* Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
* @author Vova Feldman (@svovaf)
* @author Leo Fajardo (@leorw)
function _add_deactivation_feedback_dialog_box() {
( is_object( $this->_site ) && ! $this->is_registered() )
$subscription_cancellation_dialog_box_template_params = $this->apply_filters( 'show_deactivation_subscription_cancellation', true ) ?
$this->_get_subscription_cancellation_dialog_box_template_params() :
* @since 2.3.0 Developers can optionally hide the deactivation feedback form using the 'show_deactivation_feedback_form' filter.
$show_deactivation_feedback_form = ! self::is_deactivation_snoozed();
if ( $this->has_filter( 'show_deactivation_feedback_form' ) ) {
$show_deactivation_feedback_form = $this->apply_filters( 'show_deactivation_feedback_form', true );
} else if ( $this->is_addon() ) {
* If the add-on's 'show_deactivation_feedback_form' is not set, try to inherit the value from the parent.
$show_deactivation_feedback_form = $this->get_parent_instance()->apply_filters( 'show_deactivation_feedback_form', true );
$uninstall_confirmation_message = $this->apply_filters( 'uninstall_confirmation_message', '' );
empty( $subscription_cancellation_dialog_box_template_params ) &&
! $show_deactivation_feedback_form &&
empty( $uninstall_confirmation_message )
$vars = array( 'id' => $this->_module_id );
if ( $show_deactivation_feedback_form ) {
/* Check the type of user:
* 1. Long-term (long-term)
* 2. Non-registered and non-anonymous short-term (non-registered-and-non-anonymous-short-term).
* 3. Short-term (short-term)
$is_long_term_user = true;
// Check if the site is at least 2 days old.
$time_installed = $this->_storage->install_timestamp;
// Difference in seconds.
$date_diff = time() - $time_installed;
// Convert seconds to days.
$date_diff_days = floor( $date_diff / ( 60 * 60 * 24 ) );
if ( $date_diff_days < 2 ) {
$is_long_term_user = false;
$is_long_term_user = $this->apply_filters( 'is_long_term_user', $is_long_term_user );
if ( $is_long_term_user ) {
$user_type = 'long-term';
if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
$user_type = 'non-registered-and-non-anonymous-short-term';
$user_type = 'short-term';
$uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
$vars['reasons'] = $uninstall_reasons;
$vars['subscription_cancellation_dialog_box_template_params'] = &$subscription_cancellation_dialog_box_template_params;
$vars['show_deactivation_feedback_form'] = $show_deactivation_feedback_form;
$vars['uninstall_confirmation_message'] = $uninstall_confirmation_message;
* Load the HTML template for the deactivation feedback dialog box.
* @todo Deactivation form core functions should be loaded only once! Otherwise, when there are multiple Freemius powered plugins the same code is loaded multiple times. The only thing that should be loaded differently is the various deactivation reasons object based on the state of the plugin.
fs_require_template( 'forms/deactivation/form.php', $vars );
* @author Leo Fajardo (@leorw)
* @param string $user_type
* @return array The uninstall reasons for the specified user type.
function _get_uninstall_reasons( $user_type = 'long-term' ) {
$module_type = $this->_module_type;
$internal_message_template_var = array(
'id' => $this->_module_id
$plan = $this->get_plan();
if ( $this->is_registered() && is_object( $plan ) && $plan->has_technical_support() ) {
$contact_support_template = fs_get_template( 'forms/deactivation/contact.php', $internal_message_template_var );
$contact_support_template = '';
$reason_found_better_plugin = array(
'id' => self::REASON_FOUND_A_BETTER_PLUGIN,
'text' => sprintf( $this->get_text_inline( 'I found a better %s', 'reason-found-a-better-plugin' ), $module_type ),
'input_type' => 'textfield',
'input_placeholder' => sprintf( $this->get_text_inline( "What's the %s's name?", 'placeholder-plugin-name' ), $module_type ),
$reason_temporary_deactivation = array(
'id' => self::REASON_TEMPORARY_DEACTIVATION,
$this->get_text_inline( "It's a temporary %s - I'm troubleshooting an issue", 'reason-temporary-x' ),
strtolower( $this->is_plugin() ?
$this->get_text_inline( 'Deactivation', 'deactivation' ) :
$this->get_text_inline( 'Theme Switch', 'theme-switch' )
'input_placeholder' => ''
'id' => self::REASON_OTHER,
'text' => $this->get_text_inline( 'Other', 'reason-other' ),
'input_type' => 'textfield',
'input_placeholder' => ''
$long_term_user_reasons = array(
'id' => self::REASON_NO_LONGER_NEEDED,
'text' => sprintf( $this->get_text_inline( 'I no longer need the %s', 'reason-no-longer-needed' ), $module_type ),
'input_placeholder' => ''
$reason_found_better_plugin,
'id' => self::REASON_NEEDED_FOR_A_SHORT_PERIOD,
'text' => sprintf( $this->get_text_inline( 'I only needed the %s for a short period', 'reason-needed-for-a-short-period' ), $module_type ),
'input_placeholder' => ''