store_id_slug_type_path_map( $module_id, $slug );
}
$this->_module_id = $module_id;
$this->_slug = $this->get_slug();
$this->_module_type = $this->get_module_type();
$this->_blog_id = is_multisite() ? get_current_blog_id() : null;
$this->_storage = FS_Storage::instance( $this->_module_type, $this->_slug );
$this->_cache = FS_Cache_Manager::get_manager( WP_FS___OPTION_PREFIX . "cache_{$module_id}" );
$this->_logger = FS_Logger::get_logger( WP_FS__SLUG . '_' . $this->get_unique_affix(), WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
$this->_plugin_main_file_path = $this->_find_caller_plugin_file( $is_init );
$this->_plugin_dir_path = plugin_dir_path( $this->_plugin_main_file_path );
$this->_plugin_basename = $this->get_plugin_basename();
$this->_free_plugin_basename = str_replace( '-premium/', '/', $this->_plugin_basename );
$this->_is_multisite_integrated = (
defined( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) &&
( true === constant( "WP_FS__PRODUCT_{$module_id}_MULTISITE" ) )
);
$this->_is_network_active = (
is_multisite() &&
$this->_is_multisite_integrated &&
// Themes are always network activated, but the ACTUAL activation is per site.
$this->is_plugin() &&
( is_plugin_active_for_network( $this->_plugin_basename ) ||
// Plugin network level activation or uninstall.
is_plugin_inactive( $this->_plugin_basename ) )
);
$this->_storage->set_network_active(
$this->_is_network_active,
$this->is_delegated_connection()
);
#region Migration
if ( is_multisite() ) {
/**
* If the install_timestamp exists on the site level but doesn't exist on the
* network level storage, it means that we need to process the storage with migration.
*
* The code in this `if` scope will only be executed once and only for the first site that will execute it because once we migrate the storage data, install_timestamp will be already set in the network level storage.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*/
if ( false === $this->_storage->get( 'install_timestamp', false, true ) &&
false !== $this->_storage->get( 'install_timestamp', false, false )
) {
// Initiate storage migration.
$this->_storage->migrate_to_network();
// Migrate module cache to network level storage.
$this->_cache->migrate_to_network();
}
}
#endregion
$base_name_split = explode( '/', $this->_plugin_basename );
$this->_plugin_dir_name = $base_name_split[0];
if ( $this->_logger->is_on() ) {
$this->_logger->info( 'plugin_main_file_path = ' . $this->_plugin_main_file_path );
$this->_logger->info( 'plugin_dir_path = ' . $this->_plugin_dir_path );
$this->_logger->info( 'plugin_basename = ' . $this->_plugin_basename );
$this->_logger->info( 'free_plugin_basename = ' . $this->_free_plugin_basename );
$this->_logger->info( 'plugin_dir_name = ' . $this->_plugin_dir_name );
}
// Remember link between file to slug.
$this->store_file_slug_map();
// Store plugin's initial install timestamp.
if ( ! isset( $this->_storage->install_timestamp ) ) {
$this->_storage->install_timestamp = WP_FS__SCRIPT_START_TIME;
}
if ( ! is_object( $this->_plugin ) ) {
$this->_plugin = FS_Plugin_Manager::instance( $this->_module_id )->get();
}
$this->_admin_notices = FS_Admin_Notices::instance(
$this->_slug . ( $this->is_theme() ? ':theme' : '' ),
/**
* Ensure that the admin notice will always have a title by using the stored plugin title if available and
* retrieving the title via the "get_plugin_name" method if there is no stored plugin title available.
*
* @author Leo Fajardo (@leorw)
* @since 1.2.2
*/
( is_object( $this->_plugin ) ? $this->_plugin->title : $this->get_plugin_name() ),
$this->get_unique_affix()
);
if ( 'true' === fs_request_get( 'fs_clear_api_cache' ) ||
'true' === fs_request_is_action( 'restart_freemius' )
) {
FS_Api::clear_cache();
$this->_cache->clear();
}
$this->_register_hooks();
/**
* Starting from version 2.0.0, `FS_Site` entities no longer have the `plan` property and have `plan_id`
* instead. This should be called before calling `_load_account()`, otherwise, `$this->_site` will not be
* loaded in `_load_account` for versions of SDK starting from 2.0.0.
*
* @author Leo Fajardo (@leorw)
*/
self::migrate_install_plan_to_plan_id( $this->_storage );
$this->_load_account();
$this->_version_updates_handler();
}
/**
* Checks whether this module has a settings menu.
*
* @author Leo Fajardo (@leorw)
* @since 1.2.2
*
* @return bool
*/
function has_settings_menu() {
return ( $this->_is_network_active && fs_is_network_admin() ) ?
$this->_menu->has_network_menu() :
$this->_menu->has_menu();
}
/**
* Check if the context module is free wp.org theme.
*
* This method is helpful because:
* 1. wp.org themes are limited to a single submenu item,
* and sub-submenu items are most likely not allowed (never verified).
* 2. wp.org themes are not allowed to redirect the user
* after the theme activation, therefore, the agreed UX
* is showing the opt-in as a modal dialog box after
* activation (approved by @otto42, @emiluzelac, @greenshady, @grapplerulrich).
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*
* @return bool
*/
function is_free_wp_org_theme() {
return (
$this->is_theme() &&
$this->is_org_repo_compliant() &&
! $this->is_premium()
);
}
/**
* Checks whether this a submenu item is visible.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.6
* @since 1.2.2.7 Even if the menu item was specified to be hidden, when it is the context page, then show the submenu item so the user will have the right context page.
*
* @param string $slug
* @param bool $ignore_free_wp_org_theme_context This is used to decide if the associated tab should be shown
* or hidden.
*
* @return bool
*/
function is_submenu_item_visible( $slug, $ignore_free_wp_org_theme_context = false ) {
if ( $this->is_admin_page( $slug ) ) {
/**
* It is the current context page, so show the submenu item
* so the user will have the right context page, even if it
* was set to hidden.
*/
return true;
}
if ( ! $this->has_settings_menu() ) {
// No menu settings at all.
return false;
}
if ( ! $ignore_free_wp_org_theme_context && $this->is_free_wp_org_theme() ) {
/**
* wp.org themes are limited to a single submenu item, and
* sub-submenu items are most likely not allowed (never verified).
*/
return false;
}
return $this->_menu->is_submenu_item_visible( $slug );
}
/**
* Check if a Freemius page should be accessible via the UI.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*
* @param string $slug
*
* @return bool
*/
function is_page_visible( $slug ) {
if ( $this->is_admin_page( $slug ) ) {
return true;
}
return $this->_menu->is_submenu_item_visible( $slug, true, true );
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*/
private function _version_updates_handler() {
if ( ! isset( $this->_storage->sdk_version ) || $this->_storage->sdk_version != $this->version ) {
// Freemius version upgrade mode.
$this->_storage->sdk_last_version = $this->_storage->sdk_version;
$this->_storage->sdk_version = $this->version;
if ( empty( $this->_storage->sdk_last_version ) ||
version_compare( $this->_storage->sdk_last_version, $this->version, '<' )
) {
$this->_storage->sdk_upgrade_mode = true;
$this->_storage->sdk_downgrade_mode = false;
} else {
$this->_storage->sdk_downgrade_mode = true;
$this->_storage->sdk_upgrade_mode = false;
}
$this->do_action( 'sdk_version_update', $this->_storage->sdk_last_version, $this->version );
}
$plugin_version = $this->get_plugin_version();
if ( ! isset( $this->_storage->plugin_version ) || $this->_storage->plugin_version != $plugin_version ) {
// Plugin version upgrade mode.
$this->_storage->plugin_last_version = $this->_storage->plugin_version;
$this->_storage->plugin_version = $plugin_version;
if ( empty( $this->_storage->plugin_last_version ) ||
version_compare( $this->_storage->plugin_last_version, $plugin_version, '<' )
) {
$this->_storage->plugin_upgrade_mode = true;
$this->_storage->plugin_downgrade_mode = false;
} else {
$this->_storage->plugin_downgrade_mode = true;
$this->_storage->plugin_upgrade_mode = false;
}
if ( ! empty( $this->_storage->plugin_last_version ) ) {
// Different version of the plugin was installed before, therefore it's an update.
$this->_storage->is_plugin_new_install = false;
}
$this->do_action( 'plugin_version_update', $this->_storage->plugin_last_version, $plugin_version );
}
}
#--------------------------------------------------------------------------------
#region Data Migration on SDK Update
#--------------------------------------------------------------------------------
/**
* @author Vova Feldman (@svovaf)
* @since 1.1.5
*
* @param string $sdk_prev_version
* @param string $sdk_version
*/
function _sdk_version_update( $sdk_prev_version, $sdk_version ) {
/**
* @since 1.1.7.3 Fixed unwanted connectivity test cleanup.
*/
if ( empty( $sdk_prev_version ) ) {
return;
}
if ( version_compare( $sdk_prev_version, '2.1.0', '<' ) &&
version_compare( $sdk_version, '2.1.0', '>=' )
) {
$this->_storage->handle_gdpr_admin_notice = true;
}
if ( version_compare( $sdk_prev_version, '2.0.0', '<' ) &&
version_compare( $sdk_version, '2.0.0', '>=' )
) {
$this->migrate_to_subscriptions_collection();
$this->consolidate_licenses();
// Clear trial_plan since it's now loaded from the plans collection when needed.
$this->_storage->remove( 'trial_plan', true, false );
}
if ( version_compare( $sdk_prev_version, '1.2.3', '<' ) &&
version_compare( $sdk_version, '1.2.3', '>=' )
) {
/**
* Starting from version 1.2.3, paths are stored as relative paths and not absolute paths; so when upgrading to 1.2.3, make paths relative.
*
* @author Leo Fajardo (@leorw)
*/
$this->make_paths_relative();
}
if ( version_compare( $sdk_prev_version, '1.1.5', '<' ) &&
version_compare( $sdk_version, '1.1.5', '>=' )
) {
// On version 1.1.5 merged connectivity and is_on data.
if ( isset( $this->_storage->connectivity_test ) ) {
if ( ! isset( $this->_storage->is_on ) ) {
unset( $this->_storage->connectivity_test );
} else {
$connectivity_data = $this->_storage->connectivity_test;
$connectivity_data['is_active'] = $this->_storage->is_on['is_active'];
$connectivity_data['timestamp'] = $this->_storage->is_on['timestamp'];
// Override.
$this->_storage->connectivity_test = $connectivity_data;
// Remove previous structure.
unset( $this->_storage->is_on );
}
}
}
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.0.0
*
* @param \FS_Storage $storage
* @param bool|int|null $blog_id
*/
private static function migrate_install_plan_to_plan_id( FS_Storage $storage, $blog_id = null ) {
if ( empty( $storage->sdk_version ) ) {
// New installation of the plugin, no need to upgrade.
return;
}
if ( ! version_compare( $storage->sdk_version, '2.0.0', '<' ) ) {
// Previous version is >= 2.0.0, so no need to migrate.
return;
}
// Alias.
$module_type = $storage->get_module_type();
$module_slug = $storage->get_module_slug();
$installs = self::get_all_sites( $module_type, $blog_id );
$install = isset( $installs[ $module_slug ] ) ? $installs[ $module_slug ] : null;
if ( ! is_object( $install ) ) {
return;
}
if ( isset( $install->plan ) && is_object( $install->plan ) ) {
if ( isset( $install->plan->id ) && ! empty( $install->plan->id ) ) {
$install->plan_id = self::_decrypt( $install->plan->id );
}
unset( $install->plan );
$installs[ $module_slug ] = clone $install;
self::set_account_option_by_module(
$module_type,
'sites',
$installs,
true,
$blog_id
);
}
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.0.0
*/
private function migrate_to_subscriptions_collection() {
if ( ! is_object( $this->_site ) ) {
return;
}
if ( isset( $this->_storage->subscription ) && is_object( $this->_storage->subscription ) ) {
$this->_storage->subscriptions = array( $this->_storage->subscription );
}
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.0.0
*/
private function consolidate_licenses() {
$plugin_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_PLUGIN );
if ( isset( $plugin_licenses[ $this->_slug ] ) ) {
$plugin_licenses = $plugin_licenses[ $this->_slug ];
} else {
$plugin_licenses = array();
}
$theme_licenses = self::get_account_option( 'licenses', WP_FS__MODULE_TYPE_THEME );
if ( isset( $theme_licenses[ $this->_slug ] ) ) {
$theme_licenses = $theme_licenses[ $this->_slug ];
} else {
$theme_licenses = array();
}
if ( empty( $plugin_licenses ) && empty( $theme_licenses ) ) {
return;
}
$all_licenses = array();
$user_id_license_ids_map = array();
foreach ( $plugin_licenses as $user_id => $user_licenses ) {
if ( is_array( $user_licenses ) ) {
if ( ! isset( $user_license_ids[ $user_id ] ) ) {
$user_id_license_ids_map[ $user_id ] = array();
}
foreach ( $user_licenses as $user_license ) {
$all_licenses[] = $user_license;
$user_id_license_ids_map[ $user_id ][] = $user_license->id;
}
}
}
foreach ( $theme_licenses as $user_id => $user_licenses ) {
if ( is_array( $user_licenses ) ) {
if ( ! isset( $user_license_ids[ $user_id ] ) ) {
$user_id_license_ids_map[ $user_id ] = array();
}
foreach ( $user_licenses as $user_license ) {
$all_licenses[] = $user_license;
$user_id_license_ids_map[ $user_id ][] = $user_license->id;
}
}
}
self::store_user_id_license_ids_map(
$user_id_license_ids_map,
$this->_module_id
);
$this->_store_licenses( true, $this->_module_id, $all_licenses );
}
/**
* Makes paths relative.
*
* @author Leo Fajardo (@leorw)
* @since 1.2.3
*/
private function make_paths_relative() {
$id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
if ( isset( $id_slug_type_path_map[ $this->_module_id ]['path'] ) ) {
$id_slug_type_path_map[ $this->_module_id ]['path'] = $this->get_relative_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
}
if ( isset( $this->_storage->plugin_main_file ) ) {
$plugin_main_file = $this->_storage->plugin_main_file;
if ( isset( $plugin_main_file->path ) ) {
$this->_storage->plugin_main_file->path = $this->get_relative_path( $this->_storage->plugin_main_file->path );
} else if ( isset( $plugin_main_file->prev_path ) ) {
$this->_storage->plugin_main_file->prev_path = $this->get_relative_path( $this->_storage->plugin_main_file->prev_path );
}
}
// Remove invalid path that is still associated with the current slug if there's any.
$file_slug_map = self::$_accounts->get_option( 'file_slug_map', array() );
foreach ( $file_slug_map as $plugin_basename => $slug ) {
if ( $slug === $this->_slug &&
$plugin_basename !== $this->_plugin_basename &&
! file_exists( $this->get_absolute_path( $plugin_basename ) )
) {
unset( $file_slug_map[ $plugin_basename ] );
self::$_accounts->set_option( 'file_slug_map', $file_slug_map, true );
break;
}
}
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*
* @param string $plugin_prev_version
* @param string $plugin_version
*/
function _after_version_update( $plugin_prev_version, $plugin_version ) {
if ( $this->is_theme() ) {
// Expire the cache of the previous tabs since the theme may
// have setting updates.
$this->_cache->expire( 'tabs' );
$this->_cache->expire( 'tabs_stylesheets' );
}
}
/**
* A special migration logic for the $_accounts, executed for all the plugins in the system:
* - Moves some data to the network level storage.
* - If the plugin's connection was skipped for all sites, set the plugin as if it was network skipped.
* - If the plugin's connection was ignored for all sites, don't do anything in terms of the network connection.
* - If the plugin was connected to all sites by the same super-admin, set the plugin as if was network opted-in for all sites.
* - If there's at least one site that was connected by a super-admin, find the "main super-admin" (the one that installed the majority of the plugin installs) and set the plugin as if was network activated with the main super-admin, set all the sites that were skipped or opted-in with a different user to delegated mode. Then, prompt the currently logged super-admin to choose what to do with the ignored sites.
* - If there are any sites in the network which the connection decision was not yet taken for, set this plugin into network activation mode so a super-admin can choose what to do with the rest of the sites.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*/
private static function migrate_accounts_to_network() {
$sites = self::get_sites();
$sites_count = count( $sites );
$connection_status = array();
$plugin_slugs = array();
foreach ( $sites as $site ) {
$blog_id = self::get_site_blog_id( $site );
self::$_accounts->migrate_to_network( $blog_id );
/**
* Build a list of all Freemius powered plugins slugs.
*/
$id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array(), $blog_id );
foreach ( $id_slug_type_path_map as $module_id => $data ) {
if ( WP_FS__MODULE_TYPE_PLUGIN === $data['type'] ) {
$plugin_slugs[ $data['slug'] ] = true;
}
}
$installs = self::get_account_option( 'sites', WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
if ( is_array( $installs ) ) {
foreach ( $installs as $slug => $install ) {
if ( ! isset( $connection_status[ $slug ] ) ) {
$connection_status[ $slug ] = array();
}
if ( is_object( $install ) &&
FS_Site::is_valid_id( $install->id ) &&
FS_User::is_valid_id( $install->user_id )
) {
$connection_status[ $slug ][ $blog_id ] = $install->user_id;
}
}
}
}
foreach ( $plugin_slugs as $slug => $true ) {
if ( ! isset( $connection_status[ $slug ] ) ) {
$connection_status[ $slug ] = array();
}
foreach ( $sites as $site ) {
$blog_id = self::get_site_blog_id( $site );
if ( isset( $connection_status[ $slug ][ $blog_id ] ) ) {
continue;
}
$storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
$is_anonymous = $storage->get( 'is_anonymous', null, $blog_id );
if ( ! is_null( $is_anonymous ) ) {
// Since 1.1.3 is_anonymous is an array.
if ( is_array( $is_anonymous ) && isset( $is_anonymous['is'] ) ) {
$is_anonymous = $is_anonymous['is'];
}
if ( is_bool( $is_anonymous ) && true === $is_anonymous ) {
$connection_status[ $slug ][ $blog_id ] = 'skipped';
}
}
if ( ! isset( $connection_status[ $slug ][ $blog_id ] ) ) {
$connection_status[ $slug ][ $blog_id ] = 'ignored';
}
}
}
$super_admins = array();
foreach ( $connection_status as $slug => $blogs_status ) {
$skips = 0;
$ignores = 0;
$connections = 0;
$opted_in_users = array();
$opted_in_super_admins = array();
$storage = FS_Storage::instance( WP_FS__MODULE_TYPE_PLUGIN, $slug );
foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
if ( 'skipped' === $status_or_user_id ) {
$skips ++;
} else if ( 'ignored' === $status_or_user_id ) {
$ignores ++;
} else if ( FS_User::is_valid_id( $status_or_user_id ) ) {
$connections ++;
if ( ! isset( $opted_in_users[ $status_or_user_id ] ) ) {
$opted_in_users[ $status_or_user_id ] = array();
}
$opted_in_users[ $status_or_user_id ][] = $blog_id;
if ( isset( $super_admins[ $status_or_user_id ] ) ||
self::is_super_admin( $status_or_user_id )
) {
// Cache super-admin data.
$super_admins[ $status_or_user_id ] = true;
// Remember opted-in super-admins for the plugin.
$opted_in_super_admins[ $status_or_user_id ] = true;
}
}
}
$main_super_admin_user_id = null;
$all_migrated = false;
if ( $sites_count == $skips ) {
// All sites were skipped -> network skip by copying the anonymous mode from any of the sites.
$storage->is_anonymous_ms = $storage->is_anonymous;
$all_migrated = true;
} else if ( $sites_count == $ignores ) {
// Don't do anything, still in activation mode.
$all_migrated = true;
} else if ( 0 < count( $opted_in_super_admins ) ) {
// Find the super-admin with the majority of installs.
$max_installs_by_super_admin = 0;
foreach ( $opted_in_super_admins as $user_id => $true ) {
$installs_count = count( $opted_in_users[ $user_id ] );
if ( $installs_count > $max_installs_by_super_admin ) {
$max_installs_by_super_admin = $installs_count;
$main_super_admin_user_id = $user_id;
}
}
if ( $sites_count == $connections && 1 == count( $opted_in_super_admins ) ) {
// Super-admin opted-in for all sites in the network.
$storage->is_network_connected = true;
$all_migrated = true;
}
// Store network user.
$storage->network_user_id = $main_super_admin_user_id;
$storage->network_install_blog_id = ( $sites_count == $connections ) ?
// Since all sites are opted-in, associating with the main site.
get_current_blog_id() :
// Associating with the 1st found opted-in site.
$opted_in_users[ $main_super_admin_user_id ][0];
/**
* Make sure we migrate the plan ID of the network install, otherwise, if after the migration
* the 1st page that will be loaded is the network level WP Admin and $storage->network_install_blog_id
* is different than the main site of the network, the $this->_site will not be set since the plan_id
* will be empty.
*/
$storage->migrate_to_network();
self::migrate_install_plan_to_plan_id( $storage, $storage->network_install_blog_id );
} else {
// At least one opt-in. All the opt-in were created by a non-super-admin.
if ( 0 == $ignores ) {
// All sites were opted-in or skipped, all by non-super-admin. So delegate all.
$storage->store( 'is_delegated_connection', true, true );
$all_migrated = true;
}
}
if ( ! $all_migrated ) {
/**
* Delegate all sites that were:
* 1) Opted-in by a user that is NOT the main-super-admin.
* 2) Skipped and non of the sites was opted-in by a super-admin. If any site was opted-in by a super-admin, there will be a main-super-admin, and we consider the skip as if it was done by that user.
*/
foreach ( $blogs_status as $blog_id => $status_or_user_id ) {
if ( $status_or_user_id == $main_super_admin_user_id ) {
continue;
}
if ( FS_User::is_valid_id( $status_or_user_id ) ||
( 'skipped' === $status_or_user_id && is_null( $main_super_admin_user_id ) )
) {
$storage->store( 'is_delegated_connection', true, $blog_id );
}
}
}
if ( ( $connections + $skips > 0 ) ) {
if ( $ignores > 0 ) {
/**
* If admin already opted-in or skipped in any of the network sites, and also
* have sites which the connection decision was not yet taken, set this plugin
* into network activation mode so the super-admin can choose what to do with
* the rest of the sites.
*/
self::set_network_upgrade_mode( $storage );
}
}
}
}
/**
* Set a module into network upgrade mode.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*
* @param \FS_Storage $storage
*
* @return bool
*/
private static function set_network_upgrade_mode( FS_Storage $storage ) {
return $storage->is_network_activation = true;
}
/**
* Will return true after upgrading to the SDK with the network level integration,
* when the super-admin involvement is required regarding the rest of the sites.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*
* @return bool
*/
function is_network_upgrade_mode() {
return $this->_storage->get( 'is_network_activation' );
}
/**
* Clear flag after the upgrade mode completion.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*
* @return bool True if network activation was on and now completed.
*/
private function network_upgrade_mode_completed() {
if ( fs_is_network_admin() && $this->is_network_upgrade_mode() ) {
$this->_storage->remove( 'is_network_activation' );
return true;
}
return false;
}
#endregion
/**
* This action is connected to the 'plugins_loaded' hook and helps to determine
* if this is a new plugin installation or a plugin update.
*
* There are 3 different use-cases:
* 1) New plugin installation right with Freemius:
* 1.1 _activate_plugin_event_hook() will be executed first
* 1.2 Since $this->_storage->is_plugin_new_install is not set,
* and $this->_storage->plugin_last_version is not set,
* $this->_storage->is_plugin_new_install will be set to TRUE.
* 1.3 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
* be already set to TRUE.
*
* 2) Plugin update, didn't have Freemius before, and now have the SDK:
* 2.1 _activate_plugin_event_hook() will not be executed, because
* the activation hook do NOT fires on updates since WP 3.1.
* 2.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install will
* be empty, therefore, it will be set to FALSE.
*
* 3) Plugin update, had Freemius in prev version as well:
* 3.1 _version_updates_handler() will be executed 1st, since FS was installed
* before, $this->_storage->plugin_last_version will NOT be empty,
* therefore, $this->_storage->is_plugin_new_install will be set to FALSE.
* 3.2 When _plugins_loaded() will be executed, $this->_storage->is_plugin_new_install is
* already set, therefore, it will not be modified.
*
* Use-case #3 is backward compatible, #3.1 will be executed since 1.0.9.
*
* NOTE:
* The only fallback of this mechanism is if an admin updates a plugin based on use-case #2,
* and then, the next immediate PageView is the plugin's main settings page, it will not
* show the opt-in right away. The reason it will happen is because Freemius execution
* will be turned off till the plugin is fully loaded at least once
* (till $this->_storage->was_plugin_loaded is TRUE).
*
* @author Vova Feldman (@svovaf)
* @since 1.1.9
*
*/
function _plugins_loaded() {
// Update flag that plugin was loaded with Freemius at least once.
$this->_storage->was_plugin_loaded = true;
/**
* Bug fix - only set to false when it's a plugin, due to the
* execution sequence of the theme hooks and our methods, if
* this will be set for themes, Freemius will always assume
* it's a theme update.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.2
*/
if ( $this->is_plugin() &&
! isset( $this->_storage->is_plugin_new_install )
) {
$this->_storage->is_plugin_new_install = false;
}
}
/**
* Add special parameter to WP admin AJAX calls so when we
* process AJAX calls we can identify its source properly.
*
* @author Leo Fajardo (@leorw)
* @since 2.0.0
*/
static function _enrich_ajax_url() {
$admin_param = is_network_admin() ?
'_fs_network_admin' :
'_fs_blog_admin';
?>
_logger->entrance();
if ( is_admin() ) {
add_action( 'plugins_loaded', array( &$this, '_hook_action_links_and_register_account_hooks' ) );
if ( $this->is_plugin() ) {
$plugin_dir = dirname( $this->_plugin_dir_path ) . '/';
/**
* @since 1.2.2
*
* Hook to both free and premium version activations to support
* auto deactivation on the other version activation.
*/
register_activation_hook(
$plugin_dir . $this->_free_plugin_basename,
array( &$this, '_activate_plugin_event_hook' )
);
register_activation_hook(
$plugin_dir . $this->premium_plugin_basename(),
array( &$this, '_activate_plugin_event_hook' )
);
} else {
add_action( 'after_switch_theme', array( &$this, '_activate_theme_event_hook' ), 10, 2 );
/**
* Include the required hooks to capture the theme settings' page tabs
* and cache them.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*/
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 );
}
add_action(
'admin_footer',
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
* with priority 10.
*
* @author Vova Feldman (@svovaf)
*/
11
);
add_action( 'admin_footer', array( &$this, '_style_premium_theme' ) );
}
/**
* Part of the mechanism to identify new plugin install vs. plugin update.
*
* @author Vova Feldman (@svovaf)
* @since 1.1.9
*/
if ( empty( $this->_storage->was_plugin_loaded ) ) {
/**
* During the plugin activation (not theme), 'plugins_loaded' will be already executed
* when the logic gets here since the activation logic first add the activate plugins,
* then triggers 'plugins_loaded', and only then include the code of the plugin that
* is activated. Which means that _plugins_loaded() will NOT be executed during the
* plugin activation, and that IS intentional.
*
* @author Vova Feldman (@svovaf)
*/
if ( $this->is_plugin() && $this->is_activation_mode( false ) ) {
add_action( 'plugins_loaded', array( &$this, '_plugins_loaded' ) );
} else {
// If was activated before, then it was already loaded before.
$this->_plugins_loaded();
}
}
if ( ! self::is_ajax() ) {
if ( ! $this->is_addon() ) {
add_action( 'init', array( &$this, '_add_default_submenu_items' ), WP_FS__LOWEST_PRIORITY );
}
}
if ( $this->_storage->handle_gdpr_admin_notice ) {
add_action( 'init', array( &$this, '_maybe_show_gdpr_admin_notice' ) );
}
add_action( 'init', array( &$this, '_maybe_add_gdpr_optin_ajax_handler') );
}
if ( $this->is_plugin() ) {
if ( $this->_is_network_active ) {
add_action( 'wpmu_new_blog', array( $this, '_after_new_blog_callback' ), 10, 6 );
}
register_deactivation_hook( $this->_plugin_main_file_path, array( &$this, '_deactivate_plugin_hook' ) );
}
if ( is_multisite() ) {
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' ) );
add_action( 'deleted_blog', array( &$this, '_after_site_deleted_callback' ), 10, 2 );
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() &&
self::is_customizer() &&
$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() ) {
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' ) );
$this->add_ajax_action( 'update_billing', array( &$this, '_update_billing_ajax_action' ) );
$this->add_ajax_action( 'start_trial', array( &$this, '_start_trial_ajax_action' ) );
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(
&$this,
'_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 );
$this->add_action(
'plugin_version_update',
array( &$this, '_after_version_update' ),
WP_FS__DEFAULT_PRIORITY,
2
);
$this->add_filter( 'after_code_type_change', array( &$this, '_after_code_type_change' ) );
add_action( 'admin_init', array( &$this, '_add_trial_notice' ) );
add_action( 'admin_init', array( &$this, '_add_affiliate_program_notice' ) );
add_action( 'admin_enqueue_scripts', array( &$this, '_enqueue_common_css' ) );
/**
* Handle request to reset anonymous mode for `get_reconnect_url()`.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.1.5
*/
if ( fs_request_is_action( 'reset_anonymous_mode' ) &&
$this->get_unique_affix() === fs_request_get( 'fs_unique_affix' )
) {
add_action( 'admin_init', array( &$this, 'connect_again' ) );
}
}
/**
* 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)
*
* @since 1.2.0
*/
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.
*/
private function clear_module_main_file_cache() {
if ( ! isset( $this->_storage->plugin_main_file ) ||
empty( $this->_storage->plugin_main_file->path )
) {
return;
}
$plugin_main_file = clone $this->_storage->plugin_main_file;
// Store cached path (2nd layer cache).
$plugin_main_file->prev_path = $plugin_main_file->path;
// Clear cached path.
unset( $plugin_main_file->path );
$this->_storage->plugin_main_file = $plugin_main_file;
/**
* Clear global cached path.
*
* @author Leo Fajardo (@leorw)
* @since 1.2.2
*/
$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)
* @since 2.0.0
*/
function _hook_action_links_and_register_account_hooks() {
add_action( 'admin_init', array( &$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)
* @since 1.0.9
*/
private function _register_account_hooks() {
if ( ! is_admin() ) {
return;
}
/**
* Always show the deactivation feedback form since we added
* automatic free version deactivation upon premium code activation.
*
* @since 1.2.1.6
*/
$this->add_ajax_action(
'submit_uninstall_reason',
array( &$this, '_submit_uninstall_reason_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.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.6
*
* @param bool $is_init Is initiation sequence.
*
* @return string
*/
private function _find_caller_plugin_file( $is_init = false ) {
// 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 ( isset( $plugin_main_file->path ) ) {
$absolute_path = $this->get_absolute_path( $plugin_main_file->path );
if ( file_exists( $absolute_path ) ) {
return $absolute_path;
}
}
}
/**
* @since 1.2.1
*
* `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.
*/
if ( ! $is_init ) {
// Fetch prev path cache.
if ( isset( $this->_storage->plugin_main_file ) &&
isset( $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 ) ) {
return $absolute_path;
}
}
wp_die(
$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 )
);
}
/**
* @since 1.2.1
*
* Only the original instantiator that calls dynamic_init can modify the module's path.
*/
// Find caller module.
$id_slug_type_path_map = self::$_accounts->get_option( 'id_slug_type_path_map', array() );
$this->_storage->plugin_main_file = (object) array(
'path' => $id_slug_type_path_map[ $this->_module_id ]['path'],
);
return $this->get_absolute_path( $id_slug_type_path_map[ $this->_module_id ]['path'] );
}
/**
* @author Leo Fajardo (@leorw)
* @since 1.2.3
*
* @param string $path
*
* @return string
*/
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 ) );
}
return $path;
}
/**
* @author Leo Fajardo (@leorw)
* @since 1.2.3
*
* @param string $path
* @param string|bool $module_type
*
* @return string
*/
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 );
}
return $path;
}
/**
* @author Leo Fajardo (@leorw)
* @since 1.2.3
*
* @param string|bool $module_type
*
* @return string
*/
private function get_module_root_dir_path( $module_type = false ) {
$is_plugin = empty( $module_type ) ?
$this->is_plugin() :
( WP_FS__MODULE_TYPE_PLUGIN === $module_type );
return fs_normalize_path( trailingslashit( $is_plugin ?
WP_PLUGIN_DIR :
get_theme_root() ) );
}
/**
* @author Leo Fajardo (@leorw)
*
* @param number $module_id
* @param string $slug
*
* @since 1.2.2
*/
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() );
$store_option = false;
if ( ! isset( $id_slug_type_path_map[ $module_id ] ) ) {
$id_slug_type_path_map[ $module_id ] = array(
'slug' => $slug
);
$store_option = true;
}
if ( ! isset( $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)
* @since 1.2.3
*/
! file_exists( $this->get_absolute_path(
$id_slug_type_path_map[ $module_id ]['path'],
$id_slug_type_path_map[ $module_id ]['type']
) )
) {
$caller_main_file_and_type = $this->get_caller_main_file_and_type();
$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;
$store_option = true;
}
if ( $store_option ) {
self::$_accounts->set_option( 'id_slug_type_path_map', $id_slug_type_path_map, true );
}
}
/**
* Identifies the caller type: plugin or theme.
*
* @author Leo Fajardo (@leorw)
* @since 1.2.2
*
* @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.
*/
private function get_caller_main_file_and_type() {
self::require_plugin_essentials();
$all_plugins = get_plugins();
$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.
*
* @author Vova Feldman
* @since 1.2.1.7
*/
continue;
}
$all_plugins_paths[] = fs_normalize_path( realpath( WP_PLUGIN_DIR . '/' . $relative_path ) );
}
$caller_file_candidate = false;
$caller_map = array();
$module_type = WP_FS__MODULE_TYPE_PLUGIN;
$themes_dir = fs_normalize_path( get_theme_root() );
for ( $i = 1, $bt = debug_backtrace(), $len = count( $bt ); $i < $len; $i ++ ) {
if ( empty( $bt[ $i ]['file'] ) ) {
continue;
}
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.
continue;
}
if ( ! empty( $bt[ $i ]['function'] ) && in_array( $bt[ $i ]['function'], array(
'do_action',
'apply_filter',
// The string split is stupid, but otherwise, theme check
// throws info notices.
'requir' . 'e_once',
'requir' . 'e',
'includ' . 'e_once',
'includ' . 'e'
) )
) {
// Ignore call stack hooks and files inclusion.
continue;
}
$caller_file_path = fs_normalize_path( $bt[ $i ]['file'] );
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)
* @since 1.2.2.5
*/
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 );
continue;
}
}
$caller_file_hash = md5( $caller_file_path );
if ( ! isset( $caller_map[ $caller_file_hash ] ) ) {
foreach ( $all_plugins_paths as $plugin_path ) {
if ( false !== strpos( $caller_file_path, fs_normalize_path( dirname( $plugin_path ) . '/' ) ) ) {
$caller_map[ $caller_file_hash ] = fs_normalize_path( $plugin_path );
break;
}
}
}
if ( isset( $caller_map[ $caller_file_hash ] ) ) {
$module_type = WP_FS__MODULE_TYPE_PLUGIN;
$caller_file_candidate = plugin_basename( $caller_map[ $caller_file_hash ] );
}
}
return (object) array(
'module_type' => $module_type,
'path' => $caller_file_candidate
);
}
#----------------------------------------------------------------------------------
#region Deactivation Feedback Form
#----------------------------------------------------------------------------------
/**
* Displays a confirmation and feedback dialog box when the user clicks on the "Deactivate" link on the plugins
* page.
*
* @author Vova Feldman (@svovaf)
* @author Leo Fajardo (@leorw)
* @since 1.1.2
*/
function _add_deactivation_feedback_dialog_box() {
/* 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';
} else {
if ( ! $this->is_registered() && ! $this->is_anonymous() ) {
$user_type = 'non-registered-and-non-anonymous-short-term';
} else {
$user_type = 'short-term';
}
}
$uninstall_reasons = $this->_get_uninstall_reasons( $user_type );
// Load the HTML template for the deactivation feedback dialog box.
$vars = array(
'reasons' => $uninstall_reasons,
'id' => $this->_module_id
);
/**
* @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)
* @since 1.1.2
*
* @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 );
} else {
$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,
'text' => sprintf(
$this->get_text_inline( "It's a temporary %s. I'm just debugging an issue.", 'reason-temporary-x' ),
strtolower( $this->is_plugin() ?
$this->get_text_inline( 'Deactivation', 'deactivation' ) :
$this->get_text_inline( 'Theme Switch', 'theme-switch' )
)
),
'input_type' => '',
'input_placeholder' => ''
);
$reason_other = array(
'id' => self::REASON_OTHER,
'text' => $this->get_text_inline( 'Other', 'reason-other' ),
'input_type' => 'textfield',
'input_placeholder' => ''
);
$long_term_user_reasons = array(
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_type' => '',
'input_placeholder' => ''
),
$reason_found_better_plugin,
array(
'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_type' => '',
'input_placeholder' => ''
),
array(
'id' => self::REASON_BROKE_MY_SITE,
'text' => sprintf( $this->get_text_inline( 'The %s broke my site', 'reason-broke-my-site' ), $module_type ),
'input_type' => '',
'input_placeholder' => '',
'internal_message' => $contact_support_template
),
array(
'id' => self::REASON_SUDDENLY_STOPPED_WORKING,
'text' => sprintf( $this->get_text_inline( 'The %s suddenly stopped working', 'reason-suddenly-stopped-working' ), $module_type ),
'input_type' => '',
'input_placeholder' => '',
'internal_message' => $contact_support_template
)
);
if ( $this->is_paying() ) {
$long_term_user_reasons[] = array(
'id' => self::REASON_CANT_PAY_ANYMORE,
'text' => $this->get_text_inline( "I can't pay for it anymore", 'reason-cant-pay-anymore' ),
'input_type' => 'textfield',
'input_placeholder' => $this->get_text_inline( 'What price would you feel comfortable paying?', 'placeholder-comfortable-price' )
);
}
$reason_dont_share_info = array(
'id' => self::REASON_DONT_LIKE_TO_SHARE_MY_INFORMATION,
'text' => $this->get_text_inline( "I don't like to share my information with you", 'reason-dont-like-to-share-my-information' ),
'input_type' => '',
'input_placeholder' => ''
);
/**
* If the current user has selected the "don't share data" reason in the deactivation feedback modal, inform the
* user by showing additional message that he doesn't have to share data and can just choose to skip the opt-in
* (the Skip button is included in the message to show). This message will only be shown if anonymous mode is
* enabled and the user's account is currently not in pending activation state (similar to the way the Skip
* button in the opt-in form is shown/hidden).
*/
if ( $this->is_enable_anonymous() && ! $this->is_pending_activation() ) {
$reason_dont_share_info['internal_message'] = fs_get_template( 'forms/deactivation/retry-skip.php', $internal_message_template_var );
}
$uninstall_reasons = array(
'long-term' => $long_term_user_reasons,
'non-registered-and-non-anonymous-short-term' => array(
array(
'id' => self::REASON_DIDNT_WORK,
'text' => sprintf( $this->get_text_inline( "The %s didn't work", 'reason-didnt-work' ), $module_type ),
'input_type' => '',
'input_placeholder' => ''
),
$reason_dont_share_info,
$reason_found_better_plugin
),
'short-term' => array(
array(
'id' => self::REASON_COULDNT_MAKE_IT_WORK,
'text' => $this->get_text_inline( "I couldn't understand how to make it work", 'reason-couldnt-make-it-work' ),
'input_type' => '',
'input_placeholder' => '',
'internal_message' => $contact_support_template
),
$reason_found_better_plugin,
array(
'id' => self::REASON_GREAT_BUT_NEED_SPECIFIC_FEATURE,
'text' => sprintf( $this->get_text_inline( "The %s is great, but I need specific feature that you don't support", 'reason-great-but-need-specific-feature' ), $module_type ),
'input_type' => 'textarea',
'input_placeholder' => $this->get_text_inline( 'What feature?', 'placeholder-feature' )
),
array(
'id' => self::REASON_NOT_WORKING,
'text' => sprintf( $this->get_text_inline( 'The %s is not working', 'reason-not-working' ), $module_type ),
'input_type' => 'textarea',
'input_placeholder' => $this->get_text_inline( "Kindly share what didn't work so we can fix it for future users...", 'placeholder-share-what-didnt-work' )
),
array(
'id' => self::REASON_NOT_WHAT_I_WAS_LOOKING_FOR,
'text' => $this->get_text_inline( "It's not what I was looking for", 'reason-not-what-i-was-looking-for' ),
'input_type' => 'textarea',
'input_placeholder' => $this->get_text_inline( "What you've been looking for?", 'placeholder-what-youve-been-looking-for' )
),
array(
'id' => self::REASON_DIDNT_WORK_AS_EXPECTED,
'text' => sprintf( $this->get_text_inline( "The %s didn't work as expected", 'reason-didnt-work-as-expected' ), $module_type ),
'input_type' => 'textarea',
'input_placeholder' => $this->get_text_inline( 'What did you expect?', 'placeholder-what-did-you-expect' )
)
)
);
// Randomize the reasons for the current user type.
shuffle( $uninstall_reasons[ $user_type ] );
// Keep the following reasons as the last items in the list.
$uninstall_reasons[ $user_type ][] = $reason_temporary_deactivation;
$uninstall_reasons[ $user_type ][] = $reason_other;
$uninstall_reasons = $this->apply_filters( 'uninstall_reasons', $uninstall_reasons );
return $uninstall_reasons[ $user_type ];
}
/**
* Called after the user has submitted his reason for deactivating the plugin.
*
* @author Leo Fajardo (@leorw)
* @since 1.1.2
*/
function _submit_uninstall_reason_action() {
$this->_logger->entrance();
$this->check_ajax_referer( 'submit_uninstall_reason' );
$reason_id = fs_request_get( 'reason_id' );
// Check if the given reason ID is an unsigned integer.
if ( ! ctype_digit( $reason_id ) ) {
exit;
}
$reason_info = trim( fs_request_get( 'reason_info', '' ) );
if ( ! empty( $reason_info ) ) {
$reason_info = substr( $reason_info, 0, 128 );
}
$reason = (object) array(
'id' => $reason_id,
'info' => $reason_info,
'is_anonymous' => fs_request_get_bool( 'is_anonymous' )
);
$this->_storage->store( 'uninstall_reason', $reason );
/**
* If the module type is "theme", trigger the uninstall event here (on theme deactivation) since themes do
* not support uninstall hook.
*
* @author Leo Fajardo (@leorw)
* @since 1.2.2
*/
if ( $this->is_theme() ) {
if ( $this->is_premium() && ! $this->has_active_valid_license() ) {
FS_Plugin_Updater::instance( $this )->delete_update_data();
}
$this->_uninstall_plugin_event( false );
$this->remove_sdk_reference();
}
// Print '1' for successful operation.
echo 1;
exit;
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.0.2
*/
function _delete_theme_update_data_action() {
FS_Plugin_Updater::instance( $this )->delete_update_data();
}
#endregion
#----------------------------------------------------------------------------------
#region Instance
#----------------------------------------------------------------------------------
/**
* Main singleton instance.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.0
*
* @param number $module_id
* @param string|bool $slug
* @param bool $is_init Is initiation sequence.
*
* @return Freemius|false
*/
static function instance( $module_id, $slug = false, $is_init = false ) {
if ( empty( $module_id ) ) {
return false;
}
/**
* Load the essential static data prior to initiating FS_Plugin_Manager since there's an essential MS network migration logic that needs to be executed prior to the initiation.
*/
self::_load_required_static();
if ( ! is_numeric( $module_id ) ) {
if ( ! $is_init && true === $slug ) {
$is_init = true;
}
$slug = $module_id;
$module = FS_Plugin_Manager::instance( $slug )->get();
if ( is_object( $module ) ) {
$module_id = $module->id;
}
}
$key = 'm_' . $module_id;
if ( ! isset( self::$_instances[ $key ] ) ) {
self::$_instances[ $key ] = new Freemius( $module_id, $slug, $is_init );
}
return self::$_instances[ $key ];
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.6
*
* @param number $addon_id
*
* @return bool
*/
private static function has_instance( $addon_id ) {
return isset( self::$_instances[ 'm_' . $addon_id ] );
}
/**
* @author Leo Fajardo (@leorw)
* @since 1.2.2
*
* @param string|number $id_or_slug
*
* @return number|false
*/
private static function get_module_id( $id_or_slug ) {
if ( is_numeric( $id_or_slug ) ) {
return $id_or_slug;
}
foreach ( self::$_instances as $instance ) {
if ( $instance->is_plugin() && ( $id_or_slug === $instance->get_slug() ) ) {
return $instance->get_id();
}
}
return false;
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.6
*
* @param number $id
*
* @return false|Freemius
*/
static function get_instance_by_id( $id ) {
return isset ( self::$_instances[ 'm_' . $id ] ) ?
self::$_instances[ 'm_' . $id ] :
false;
}
/**
*
* @author Vova Feldman (@svovaf)
* @since 1.0.1
*
* @param $plugin_file
*
* @return false|Freemius
*/
static function get_instance_by_file( $plugin_file ) {
$slug = self::find_slug_by_basename( $plugin_file );
return ( false !== $slug ) ?
self::instance( self::get_module_id( $slug ) ) :
false;
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.6
*
* @return false|Freemius
*/
function get_parent_instance() {
return self::get_instance_by_id( $this->_plugin->parent_plugin_id );
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.6
*
* @param string|number $id_or_slug
*
* @return false|Freemius
*/
function get_addon_instance( $id_or_slug ) {
$addon_id = self::get_module_id( $id_or_slug );
return self::instance( $addon_id );
}
#endregion ------------------------------------------------------------------
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.6
*
* @return bool
*/
function is_parent_plugin_installed() {
$is_active = self::has_instance( $this->_plugin->parent_plugin_id );
if ( $is_active ) {
return true;
}
/**
* Parent module might be a theme. If that's the case, the add-on's FS
* instance will be loaded prior to the theme's FS instance, therefore,
* we need to check if it's active with a "look ahead".
*
* @author Vova Feldman
* @since 1.2.2.3
*/
global $fs_active_plugins;
if ( is_object( $fs_active_plugins ) && is_array( $fs_active_plugins->plugins ) ) {
$active_theme = wp_get_theme();
foreach ( $fs_active_plugins->plugins as $sdk => $module ) {
if ( WP_FS__MODULE_TYPE_THEME === $module->type ) {
if ( $module->plugin_path == $active_theme->get_stylesheet() ) {
// Parent module is a theme and it's currently active.
return true;
}
}
}
}
return false;
}
/**
* Check if add-on parent plugin in activation mode.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.7
*
* @return bool
*/
function is_parent_in_activation() {
$parent_fs = $this->get_parent_instance();
if ( ! is_object( $parent_fs ) ) {
return false;
}
return ( $parent_fs->is_activation_mode() );
}
/**
* Is plugin in activation mode.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.7
*
* @param bool $and_on
*
* @return bool
*/
function is_activation_mode( $and_on = true ) {
return fs_is_network_admin() ?
$this->is_network_activation_mode( $and_on ) :
$this->is_site_activation_mode( $and_on );
}
/**
* Is plugin in activation mode.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.7
*
* @param bool $and_on
*
* @return bool
*/
function is_site_activation_mode( $and_on = true ) {
return (
( $this->is_on() || ! $and_on ) &&
( ! $this->is_registered() ||
( $this->is_only_premium() && ! $this->has_features_enabled_license() ) ) &&
( ! $this->is_enable_anonymous() ||
( ! $this->is_anonymous() && ! $this->is_pending_activation() ) )
);
}
/**
* Checks if the SDK in network activation mode.
*
* @author Leo Fajardo (@leorw)
* @since 2.0.0
*
* @param bool $and_on
*
* @return bool
*/
private function is_network_activation_mode( $and_on = true ) {
if ( ! $this->_is_network_active ) {
// Not network activated.
return false;
}
if ( $this->is_network_upgrade_mode() ) {
// Special flag to enforce network activation mode to decide what to do with the sites that are not yet opted-in nor skipped.
return true;
}
if ( ! $this->is_site_activation_mode( $and_on ) ) {
// Whether the context is single site or the network, if the plugin is no longer in activation mode then it is not in network activation mode as well.
return false;
}
if ( $this->is_network_delegated_connection() ) {
// Super-admin delegated the connection to the site admins -> not activation mode.
return false;
}
if ( $this->is_network_anonymous() ) {
// Super-admin skipped the connection network wide -> not activation mode.
return false;
}
if ( $this->is_network_registered() ) {
// Super-admin connected at least one site -> not activation mode.
return false;
}
return true;
}
/**
* Check if current page is the opt-in/pending-activation page.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.1.7
*
* @return bool
*/
function is_activation_page() {
if ( $this->_menu->is_main_settings_page() ) {
return true;
}
if ( ! $this->is_activation_mode() ) {
return false;
}
// Check if current page is matching the activation page.
return $this->is_matching_url( $this->get_activation_url() );
}
/**
* Check if URL path's are matching and that all querystring
* arguments of the $sub_url exist in the $url with the same values.
*
* WARNING:
* 1. This method doesn't check if the sub/domain are matching.
* 2. Ignore case sensitivity.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.1.7
*
* @param string $sub_url
* @param string $url If argument is not set, check if the sub_url matching the current's page URL.
*
* @return bool
*/
private function is_matching_url( $sub_url, $url = '' ) {
if ( empty( $url ) ) {
$url = $_SERVER['REQUEST_URI'];
}
$url = strtolower( $url );
$sub_url = strtolower( $sub_url );
if ( parse_url( $sub_url, PHP_URL_PATH ) !== parse_url( $url, PHP_URL_PATH ) ) {
// Different path - DO NOT OVERRIDE PAGE.
return false;
}
$url_params = array();
parse_str( parse_url( $url, PHP_URL_QUERY ), $url_params );
$sub_url_params = array();
parse_str( parse_url( $sub_url, PHP_URL_QUERY ), $sub_url_params );
foreach ( $sub_url_params as $key => $val ) {
if ( ! isset( $url_params[ $key ] ) || $val != $url_params[ $key ] ) {
// Not matching query string - DO NOT OVERRIDE PAGE.
return false;
}
}
return true;
}
/**
* Get the basenames of all active plugins for specific blog. Including network activated plugins.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*
* @param int $blog_id
*
* @return string[]
*/
private static function get_active_plugins_basenames( $blog_id = 0 ) {
if ( is_multisite() && $blog_id > 0 ) {
$active_basenames = get_blog_option( $blog_id, 'active_plugins' );
} else {
$active_basenames = get_option( 'active_plugins' );
}
if ( is_multisite() ) {
$network_active_basenames = get_site_option( 'active_sitewide_plugins' );
if ( is_array( $network_active_basenames ) && ! empty( $network_active_basenames ) ) {
$active_basenames = array_merge( $active_basenames, $network_active_basenames );
}
}
return $active_basenames;
}
/**
* Get collection of all active plugins. Including network activated plugins.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*
* @param int $blog_id Since 2.0.0
*
* @return array[string]array
*/
private static function get_active_plugins( $blog_id = 0 ) {
self::require_plugin_essentials();
$active_plugin = array();
$all_plugins = get_plugins();
$active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
foreach ( $active_plugins_basenames as $plugin_basename ) {
$active_plugin[ $plugin_basename ] = $all_plugins[ $plugin_basename ];
}
return $active_plugin;
}
/**
* Get collection of all site active plugins for a specified blog.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*
* @param int $blog_id
*
* @return array[string]array
*/
private static function get_site_active_plugins( $blog_id = 0 ) {
$active_basenames = ( is_multisite() && $blog_id > 0 ) ?
get_blog_option( $blog_id, 'active_plugins' ) :
get_option( 'active_plugins' );
$active = array();
foreach ( $active_basenames as $basename ) {
$active[ $basename ] = array(
'is_active' => true,
'Version' => '1.0', // Dummy version.
'slug' => self::get_plugin_slug( $basename ),
);
}
return $active;
}
/**
* Get collection of all plugins with their activation status for a specified blog.
*
* @author Vova Feldman (@svovaf)
* @since 1.1.8
*
* @param int $blog_id Since 2.0.0
*
* @return array Key is the plugin file path and the value is an array of the plugin data.
*/
private static function get_all_plugins( $blog_id = 0 ) {
self::require_plugin_essentials();
$all_plugins = get_plugins();
$active_plugins_basenames = self::get_active_plugins_basenames( $blog_id );
foreach ( $all_plugins as $basename => &$data ) {
// By default set to inactive (next foreach update the active plugins).
$data['is_active'] = false;
// Enrich with plugin slug.
$data['slug'] = self::get_plugin_slug( $basename );
}
// Flag active plugins.
foreach ( $active_plugins_basenames as $basename ) {
if ( isset( $all_plugins[ $basename ] ) ) {
$all_plugins[ $basename ]['is_active'] = true;
}
}
return $all_plugins;
}
/**
* Get collection of all plugins and if they are network level activated.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*
* @return array Key is the plugin basename and the value is an array of the plugin data.
*/
private static function get_network_plugins() {
self::require_plugin_essentials();
$all_plugins = get_plugins();
$network_active_basenames = is_multisite() ?
get_site_option( 'active_sitewide_plugins' ) :
array();
foreach ( $all_plugins as $basename => &$data ) {
// By default set to inactive (next foreach update the active plugins).
$data['is_active'] = false;
// Enrich with plugin slug.
$data['slug'] = self::get_plugin_slug( $basename );
}
// Flag active plugins.
foreach ( $network_active_basenames as $basename ) {
if ( isset( $all_plugins[ $basename ] ) ) {
$all_plugins[ $basename ]['is_active'] = true;
}
}
return $all_plugins;
}
/**
* Cached result of get_site_transient( 'update_plugins' )
*
* @author Vova Feldman (@svovaf)
* @since 1.1.8
*
* @var object
*/
private static $_plugins_info;
/**
* Helper function to get specified plugin's slug.
*
* @author Vova Feldman (@svovaf)
* @since 1.1.8
*
* @param $basename
*
* @return string
*/
private static function get_plugin_slug( $basename ) {
if ( ! isset( self::$_plugins_info ) ) {
self::$_plugins_info = get_site_transient( 'update_plugins' );
}
$slug = '';
if ( is_object( self::$_plugins_info ) ) {
if ( isset( self::$_plugins_info->no_update ) &&
isset( self::$_plugins_info->no_update[ $basename ] ) &&
! empty( self::$_plugins_info->no_update[ $basename ]->slug )
) {
$slug = self::$_plugins_info->no_update[ $basename ]->slug;
} else if ( isset( self::$_plugins_info->response ) &&
isset( self::$_plugins_info->response[ $basename ] ) &&
! empty( self::$_plugins_info->response[ $basename ]->slug )
) {
$slug = self::$_plugins_info->response[ $basename ]->slug;
}
}
if ( empty( $slug ) ) {
// Try to find slug from FS data.
$slug = self::find_slug_by_basename( $basename );
}
if ( empty( $slug ) ) {
// Fallback to plugin's folder name.
$slug = dirname( $basename );
}
return $slug;
}
private static $_statics_loaded = false;
/**
* Load static resources.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.1
*/
private static function _load_required_static() {
if ( self::$_statics_loaded ) {
return;
}
self::$_static_logger = FS_Logger::get_logger( WP_FS__SLUG, WP_FS__DEBUG_SDK, WP_FS__ECHO_DEBUG_SDK );
self::$_static_logger->entrance();
self::$_accounts = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true );
if ( is_multisite() ) {
/**
* If the id_slug_type_path_map exists on the site level but doesn't exist on the
* network level storage, it means that we need to process the storage with migration.
*
* The code in this `if` scope will only be executed once and only for the first site that will execute it because once we migrate the storage data, id_slug_type_path_map will be already set in the network level storage.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*/
if ( null === self::$_accounts->get_option( 'id_slug_type_path_map', null, true ) &&
null !== self::$_accounts->get_option( 'id_slug_type_path_map', null, false )
) {
self::migrate_accounts_to_network();
// Migrate API options from site level to network level.
$api_network_options = FS_Option_Manager::get_manager( WP_FS__OPTIONS_OPTION_NAME, true, true );
$api_network_options->migrate_to_network();
// Migrate API cache to network level storage.
FS_Cache_Manager::get_manager( WP_FS__API_CACHE_OPTION_NAME )->migrate_to_network();
}
}
self::$_global_admin_notices = FS_Admin_Notices::instance( 'global' );
if ( ! WP_FS__DEMO_MODE ) {
add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', array(
'Freemius',
'_add_debug_section'
) );
}
add_action( "wp_ajax_fs_toggle_debug_mode", array( 'Freemius', '_toggle_debug_mode' ) );
self::add_ajax_action_static( 'get_debug_log', array( 'Freemius', '_get_debug_log' ) );
self::add_ajax_action_static( 'get_db_option', array( 'Freemius', '_get_db_option' ) );
self::add_ajax_action_static( 'set_db_option', array( 'Freemius', '_set_db_option' ) );
if ( 0 == did_action( 'plugins_loaded' ) ) {
add_action( 'plugins_loaded', array( 'Freemius', '_load_textdomain' ), 1 );
}
add_action( 'admin_footer', array( 'Freemius', '_enrich_ajax_url' ) );
self::$_statics_loaded = true;
}
#----------------------------------------------------------------------------------
#region Localization
#----------------------------------------------------------------------------------
/**
* Load framework's text domain.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.1
*/
static function _load_textdomain() {
if ( ! is_admin() ) {
return;
}
global $fs_active_plugins;
// Works both for plugins and themes.
load_plugin_textdomain(
'freemius',
false,
$fs_active_plugins->newest->sdk_path . '/languages/'
);
}
#endregion
#----------------------------------------------------------------------------------
#region Debugging
#----------------------------------------------------------------------------------
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.8
*/
static function _add_debug_section() {
if ( ! is_super_admin() ) {
// Add debug page only for super-admins.
return;
}
self::$_static_logger->entrance();
$title = sprintf( '%s [v.%s]', fs_text_inline( 'Freemius Debug' ), WP_FS__SDK_VERSION );
if ( WP_FS__DEV_MODE ) {
// Add top-level debug menu item.
$hook = FS_Admin_Menu_Manager::add_page(
$title,
$title,
'manage_options',
'freemius',
array( 'Freemius', '_debug_page_render' )
);
} else {
// Add hidden debug page.
$hook = FS_Admin_Menu_Manager::add_subpage(
null,
$title,
$title,
'manage_options',
'freemius',
array( 'Freemius', '_debug_page_render' )
);
}
if ( ! empty( $hook ) ) {
add_action( "load-$hook", array( 'Freemius', '_debug_page_actions' ) );
}
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.1.7.3
*/
static function _toggle_debug_mode() {
$is_on = fs_request_get( 'is_on', false, 'post' );
if ( fs_request_is_post() && in_array( $is_on, array( 0, 1 ) ) ) {
update_option( 'fs_debug_mode', $is_on );
// Turn on/off storage logging.
FS_Logger::_set_storage_logging( ( 1 == $is_on ) );
}
exit;
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.2.1.6
*/
static function _get_debug_log() {
$logs = FS_Logger::load_db_logs(
fs_request_get( 'filters', false, 'post' ),
! empty( $_POST['limit'] ) && is_numeric( $_POST['limit'] ) ? $_POST['limit'] : 200,
! empty( $_POST['offset'] ) && is_numeric( $_POST['offset'] ) ? $_POST['offset'] : 0
);
self::shoot_ajax_success( $logs );
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.2.1.7
*/
static function _get_db_option() {
$option_name = fs_request_get( 'option_name' );
$value = get_option( $option_name );
$result = array(
'name' => $option_name,
);
if ( false !== $value ) {
if ( ! is_string( $value ) ) {
$value = json_encode( $value );
}
$result['value'] = $value;
}
self::shoot_ajax_success( $result );
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.2.1.7
*/
static function _set_db_option() {
$option_name = fs_request_get( 'option_name' );
$option_value = fs_request_get( 'option_value' );
if ( ! empty( $option_value ) ) {
update_option( $option_name, $option_value );
}
self::shoot_ajax_success();
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.8
*/
static function _debug_page_actions() {
self::_clean_admin_content_section();
if ( fs_request_is_action( 'restart_freemius' ) ) {
check_admin_referer( 'restart_freemius' );
if ( ! is_multisite() ) {
// Clear accounts data.
self::$_accounts->clear( null, true );
} else {
$sites = self::get_sites();
foreach ( $sites as $site ) {
$blog_id = self::get_site_blog_id( $site );
self::$_accounts->clear( $blog_id, true );
}
// Clear network level storage.
self::$_accounts->clear( true, true );
}
// Clear SDK reference cache.
delete_option( 'fs_active_plugins' );
} else if ( fs_request_is_action( 'clear_updates_data' ) ) {
check_admin_referer( 'clear_updates_data' );
if ( ! is_multisite() ) {
set_site_transient( 'update_plugins', null );
set_site_transient( 'update_themes', null );
} else {
$current_blog_id = get_current_blog_id();
$sites = self::get_sites();
foreach ( $sites as $site ) {
switch_to_blog( self::get_site_blog_id( $site ) );
set_site_transient( 'update_plugins', null );
set_site_transient( 'update_themes', null );
}
switch_to_blog( $current_blog_id );
}
} else if ( fs_request_is_action( 'simulate_trial' ) ) {
check_admin_referer( 'simulate_trial' );
$fs = freemius( fs_request_get( 'module_id' ) );
// Update SDK install to at least 24 hours before.
$fs->_storage->install_timestamp = ( time() - WP_FS__TIME_24_HOURS_IN_SEC );
// Unset the trial shown timestamp.
unset( $fs->_storage->trial_promotion_shown );
} else if ( fs_request_is_action( 'simulate_network_upgrade' ) ) {
check_admin_referer( 'simulate_network_upgrade' );
$fs = freemius( fs_request_get( 'module_id' ) );
self::set_network_upgrade_mode( $fs->_storage );
} else if ( fs_request_is_action( 'delete_install' ) ) {
check_admin_referer( 'delete_install' );
self::_delete_site_by_slug(
fs_request_get( 'slug' ),
fs_request_get( 'module_type' ),
true,
fs_request_get( 'blog_id', null )
);
} else if ( fs_request_is_action( 'delete_user' ) ) {
check_admin_referer( 'delete_user' );
self::delete_user( fs_request_get( 'user_id' ) );
} else if ( fs_request_is_action( 'download_logs' ) ) {
check_admin_referer( 'download_logs' );
$download_url = FS_Logger::download_db_logs(
fs_request_get( 'filters', false, 'post' )
);
if ( false === $download_url ) {
wp_die( 'Oops... there was an error while generating the logs download file. Please try again and if it doesn\'t work contact support@freemius.com.' );
}
fs_redirect( $download_url );
}
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.0.8
*/
static function _debug_page_render() {
self::$_static_logger->entrance();
if ( ! is_multisite() ) {
$all_plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN );
$all_themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME );
} else {
$sites = self::get_sites();
$all_plugins_installs = array();
$all_themes_installs = array();
foreach ( $sites as $site ) {
$blog_id = self::get_site_blog_id( $site );
$plugins_installs = self::get_all_sites( WP_FS__MODULE_TYPE_PLUGIN, $blog_id );
foreach ( $plugins_installs as $slug => $install ) {
if ( ! isset( $all_plugins_installs[ $slug ] ) ) {
$all_plugins_installs[ $slug ] = array();
}
$install->blog_id = $blog_id;
$all_plugins_installs[ $slug ][] = $install;
}
$themes_installs = self::get_all_sites( WP_FS__MODULE_TYPE_THEME, $blog_id );
foreach ( $themes_installs as $slug => $install ) {
if ( ! isset( $all_themes_installs[ $slug ] ) ) {
$all_themes_installs[ $slug ] = array();
}
$install->blog_id = $blog_id;
$all_themes_installs[ $slug ][] = $install;
}
}
}
$licenses_by_module_type = self::get_all_licenses_by_module_type();
$vars = array(
'plugin_sites' => $all_plugins_installs,
'theme_sites' => $all_themes_installs,
'users' => self::get_all_users(),
'addons' => self::get_all_addons(),
'account_addons' => self::get_all_account_addons(),
'plugin_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_PLUGIN ],
'theme_licenses' => $licenses_by_module_type[ WP_FS__MODULE_TYPE_THEME ]
);
fs_enqueue_local_style( 'fs_debug', '/admin/debug.css' );
fs_require_once_template( 'debug.php', $vars );
}
#endregion
#----------------------------------------------------------------------------------
#region Connectivity Issues
#----------------------------------------------------------------------------------
/**
* Check if Freemius should be turned on for the current plugin install.
*
* Note:
* $this->_is_on is updated in has_api_connectivity()
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*
* @return bool
*/
function is_on() {
self::$_static_logger->entrance();
if ( isset( $this->_is_on ) ) {
return $this->_is_on;
}
// If already installed or pending then sure it's on :)
if ( $this->is_registered() || $this->is_pending_activation() ) {
$this->_is_on = true;
return true;
}
return false;
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.1.7.3
*
* @param bool $flush_if_no_connectivity
*
* @return bool
*/
private function should_run_connectivity_test( $flush_if_no_connectivity = false ) {
if ( ! isset( $this->_storage->connectivity_test ) ) {
// Connectivity test was never executed, or cache was cleared.
return true;
}
if ( WP_FS__PING_API_ON_IP_OR_HOST_CHANGES ) {
if ( WP_FS__IS_HTTP_REQUEST ) {
if ( $_SERVER['HTTP_HOST'] != $this->_storage->connectivity_test['host'] ) {
// Domain changed.
return true;
}
if ( WP_FS__REMOTE_ADDR != $this->_storage->connectivity_test['server_ip'] ) {
// Server IP changed.
return true;
}
}
}
if ( $this->_storage->connectivity_test['is_connected'] &&
$this->_storage->connectivity_test['is_active']
) {
// API connected and Freemius is active - no need to run connectivity check.
return false;
}
if ( $flush_if_no_connectivity ) {
/**
* If explicitly asked to flush when no connectivity - do it only
* if at least 10 sec passed from the last API connectivity test.
*/
return ( isset( $this->_storage->connectivity_test['timestamp'] ) &&
( WP_FS__SCRIPT_START_TIME - $this->_storage->connectivity_test['timestamp'] ) > 10 );
}
/**
* @since 1.1.7 Don't check for connectivity on plugin downgrade.
*/
$version = $this->get_plugin_version();
if ( version_compare( $version, $this->_storage->connectivity_test['version'], '>' ) ) {
// If it's a plugin version upgrade and Freemius is off or no connectivity, run connectivity test.
return true;
}
return false;
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.1.7.4
*
* @param int|null $blog_id Since 2.0.0.
* @param bool $is_gdpr_test Since 2.0.2. Perform only the GDPR test.
*
* @return object|false
*/
private function ping( $blog_id = null, $is_gdpr_test = false ) {
if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY ) {
return false;
}
$version = $this->get_plugin_version();
$is_update = $this->apply_filters( 'is_plugin_update', $this->is_plugin_update() );
return $this->get_api_plugin_scope()->ping(
$this->get_anonymous_id( $blog_id ),
array(
'is_update' => json_encode( $is_update ),
'version' => $version,
'sdk' => $this->version,
'is_admin' => json_encode( is_admin() ),
'is_ajax' => json_encode( self::is_ajax() ),
'is_cron' => json_encode( self::is_cron() ),
'is_gdpr_test' => $is_gdpr_test,
'is_http' => json_encode( WP_FS__IS_HTTP_REQUEST ),
)
);
}
/**
* Check if there's any connectivity issue to Freemius API.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*
* @param bool $flush_if_no_connectivity
*
* @return bool
*/
function has_api_connectivity( $flush_if_no_connectivity = false ) {
$this->_logger->entrance();
if ( isset( $this->_has_api_connection ) && ( $this->_has_api_connection || ! $flush_if_no_connectivity ) ) {
return $this->_has_api_connection;
}
if ( WP_FS__SIMULATE_NO_API_CONNECTIVITY &&
isset( $this->_storage->connectivity_test ) &&
true === $this->_storage->connectivity_test['is_connected']
) {
unset( $this->_storage->connectivity_test );
}
if ( ! $this->should_run_connectivity_test( $flush_if_no_connectivity ) ) {
$this->_has_api_connection = $this->_storage->connectivity_test['is_connected'];
/**
* @since 1.1.6 During dev mode, if there's connectivity - turn Freemius on regardless the configuration.
*
* @since 1.2.1.5 If the user running the premium version then ignore the 'is_active' flag and turn Freemius on to enable license key activation.
*/
$this->_is_on = $this->_storage->connectivity_test['is_active'] ||
$this->is_premium() ||
( WP_FS__DEV_MODE && $this->_has_api_connection && ! WP_FS__SIMULATE_FREEMIUS_OFF );
return $this->_has_api_connection;
}
$pong = $this->ping();
$is_connected = $this->get_api_plugin_scope()->is_valid_ping( $pong );
if ( ! $is_connected ) {
// API failure.
$this->_add_connectivity_issue_message( $pong );
}
if ( $is_connected ) {
FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
}
$this->store_connectivity_info( $pong, $is_connected );
return $this->_has_api_connection;
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.1.7.4
*
* @param object $pong
* @param bool $is_connected
*/
private function store_connectivity_info( $pong, $is_connected ) {
$this->_logger->entrance();
$version = $this->get_plugin_version();
if ( ! $is_connected || WP_FS__SIMULATE_FREEMIUS_OFF ) {
$is_active = false;
} else {
$is_active = ( isset( $pong->is_active ) && true == $pong->is_active );
}
$is_active = $this->apply_filters(
'is_on',
$is_active,
$this->is_plugin_update(),
$version
);
$this->_storage->connectivity_test = array(
'is_connected' => $is_connected,
'host' => $_SERVER['HTTP_HOST'],
'server_ip' => WP_FS__REMOTE_ADDR,
'is_active' => $is_active,
'timestamp' => WP_FS__SCRIPT_START_TIME,
// Last version with connectivity attempt.
'version' => $version,
);
$this->_has_api_connection = $is_connected;
$this->_is_on = $is_active || ( WP_FS__DEV_MODE && $is_connected && ! WP_FS__SIMULATE_FREEMIUS_OFF );
}
/**
* Force turning Freemius on.
*
* @author Vova Feldman (@svovaf)
* @since 1.1.8.1
*
* @return bool TRUE if successfully turned on.
*/
private function turn_on() {
$this->_logger->entrance();
if ( $this->is_on() || ! isset( $this->_storage->connectivity_test['is_active'] ) ) {
return false;
}
$updated_connectivity = $this->_storage->connectivity_test;
$updated_connectivity['is_active'] = true;
$updated_connectivity['timestamp'] = WP_FS__SCRIPT_START_TIME;
$this->_storage->connectivity_test = $updated_connectivity;
$this->_is_on = true;
return true;
}
/**
* Anonymous and unique site identifier (Hash).
*
* @author Vova Feldman (@svovaf)
* @since 1.1.0
*
* @param null|int $blog_id Since 2.0.0
*
* @return string
*/
function get_anonymous_id( $blog_id = null ) {
$unique_id = self::$_accounts->get_option( 'unique_id', null, $blog_id );
if ( empty( $unique_id ) || ! is_string( $unique_id ) ) {
$key = fs_strip_url_protocol( get_site_url( $blog_id ) );
$secure_auth = SECURE_AUTH_KEY;
if ( empty( $secure_auth ) || false !== strpos( $secure_auth, ' ' ) ) {
// Protect against default auth key.
$secure_auth = md5( microtime() );
}
/**
* Base the unique identifier on the WP secure authentication key. Which
* turns the key into a secret anonymous identifier. This will help us
* to avoid duplicate installs generation on the backend upon opt-in.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.3
*/
$unique_id = md5( $key . $secure_auth );
self::$_accounts->set_option( 'unique_id', $unique_id, true, $blog_id );
}
$this->_logger->departure( $unique_id );
return $unique_id;
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.1.7.4
*
* @return \WP_User
*/
static function _get_current_wp_user() {
self::require_pluggable_essentials();
self::wp_cookie_constants();
return wp_get_current_user();
}
/**
* Define cookie constants which are required by Freemius::_get_current_wp_user() since
* it uses wp_get_current_user() which needs the cookie constants set. When a plugin
* is network activated the cookie constants are only configured after the network
* plugins activation, therefore, if we don't define those constants WP will throw
* PHP warnings/notices.
*
* @author Vova Feldman (@svovaf)
* @since 2.1.1
*/
private static function wp_cookie_constants() {
if ( defined( 'LOGGED_IN_COOKIE' ) &&
( defined( 'AUTH_COOKIE' ) || defined( 'SECURE_AUTH_COOKIE' ) )
) {
return;
}
/**
* Used to guarantee unique hash cookies
*
* @since 1.5.0
*/
if ( ! defined( 'COOKIEHASH' ) ) {
$siteurl = get_site_option( 'siteurl' );
if ( $siteurl ) {
define( 'COOKIEHASH', md5( $siteurl ) );
} else {
define( 'COOKIEHASH', '' );
}
}
if ( ! defined( 'LOGGED_IN_COOKIE' ) ) {
define( 'LOGGED_IN_COOKIE', 'wordpress_logged_in_' . COOKIEHASH );
}
/**
* @since 2.5.0
*/
if ( ! defined( 'AUTH_COOKIE' ) ) {
define( 'AUTH_COOKIE', 'wordpress_' . COOKIEHASH );
}
/**
* @since 2.6.0
*/
if ( ! defined( 'SECURE_AUTH_COOKIE' ) ) {
define( 'SECURE_AUTH_COOKIE', 'wordpress_sec_' . COOKIEHASH );
}
}
/**
* @author Vova Feldman (@svovaf)
* @since 2.1.0
*
* @return int
*/
static function get_current_wp_user_id() {
$wp_user = self::_get_current_wp_user();
return $wp_user->ID;
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.2.1.7
*
* @param string $email
*
* @return bool
*/
static function is_valid_email( $email ) {
if ( false === filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
return false;
}
$parts = explode( '@', $email );
if ( 2 !== count( $parts ) || empty( $parts[1] ) ) {
return false;
}
$blacklist = array(
'admin.',
'webmaster.',
'localhost.',
'dev.',
'development.',
'test.',
'stage.',
'staging.',
);
// Make sure domain is not one of the blacklisted.
foreach ( $blacklist as $invalid ) {
if ( 0 === strpos( $parts[1], $invalid ) ) {
return false;
}
}
// Get the UTF encoded domain name.
$domain = idn_to_ascii( $parts[1] ) . '.';
return ( checkdnsrr( $domain, 'MX' ) || checkdnsrr( $domain, 'A' ) );
}
/**
* Generate API connectivity issue message.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*
* @param mixed $api_result
* @param bool $is_first_failure
*/
function _add_connectivity_issue_message( $api_result, $is_first_failure = true ) {
if ( ! $this->is_premium() && $this->_enable_anonymous ) {
// Don't add message if it's the free version and can run anonymously.
return;
}
if ( ! function_exists( 'wp_nonce_url' ) ) {
require_once ABSPATH . 'wp-includes/functions.php';
}
$current_user = self::_get_current_wp_user();
// $admin_email = get_option( 'admin_email' );
$admin_email = $current_user->user_email;
// Aliases.
$deactivate_plugin_title = $this->esc_html_inline( 'That\'s exhausting, please deactivate', 'deactivate-plugin-title' );
$deactivate_plugin_desc = $this->esc_html_inline( 'We feel your frustration and sincerely apologize for the inconvenience. Hope to see you again in the future.', 'deactivate-plugin-desc' );
$install_previous_title = $this->esc_html_inline( 'Let\'s try your previous version', 'install-previous-title' );
$install_previous_desc = $this->esc_html_inline( 'Uninstall this version and install the previous one.', 'install-previous-desc' );
$fix_issue_title = $this->esc_html_inline( 'Yes - I\'m giving you a chance to fix it', 'fix-issue-title' );
$fix_issue_desc = $this->esc_html_inline( 'We will do our best to whitelist your server and resolve this issue ASAP. You will get a follow-up email to %s once we have an update.', 'fix-issue-desc' );
/* translators: %s: product title (e.g. "Awesome Plugin" requires an access to...) */
$x_requires_access_to_api = $this->esc_html_inline( '%s requires an access to our API.', 'x-requires-access-to-api' );
$sysadmin_title = $this->esc_html_inline( 'I\'m a system administrator', 'sysadmin-title' );
$happy_to_resolve_issue_asap = $this->esc_html_inline( 'We are sure it\'s an issue on our side and more than happy to resolve it for you ASAP if you give us a chance.', 'happy-to-resolve-issue-asap' );
$message = false;
if ( is_object( $api_result ) &&
isset( $api_result->error ) &&
isset( $api_result->error->code )
) {
switch ( $api_result->error->code ) {
case 'curl_missing':
$missing_methods = '';
if ( is_array( $api_result->missing_methods ) &&
! empty( $api_result->missing_methods )
) {
foreach ( $api_result->missing_methods as $m ) {
if ( 'curl_version' === $m ) {
continue;
}
if ( ! empty( $missing_methods ) ) {
$missing_methods .= ', ';
}
$missing_methods .= sprintf( '%s', $m );
}
if ( ! empty( $missing_methods ) ) {
$missing_methods = sprintf(
'
%s %s',
$this->esc_html_inline( 'Disabled method(s):', 'curl-disabled-methods' ),
$missing_methods
);
}
}
$message = sprintf(
$x_requires_access_to_api . ' ' .
$this->esc_html_inline( 'We use PHP cURL library for the API calls, which is a very common library and usually installed and activated out of the box. Unfortunately, cURL is not activated (or disabled) on your server.', 'curl-missing-message' ) . ' ' .
$missing_methods .
' %s',
'' . $this->get_plugin_name() . '',
sprintf(
'
%s
%s
%s
',
sprintf(
'%s%s',
$this->get_text_inline( 'I don\'t know what is cURL or how to install it, help me!', 'curl-missing-no-clue-title' ),
' - ' . sprintf(
$this->get_text_inline( 'We\'ll make sure to contact your hosting company and resolve the issue. You will get a follow-up email to %s once we have an update.', 'curl-missing-no-clue-desc' ),
'' . $admin_email . ''
)
),
sprintf(
'%s - %s',
$sysadmin_title,
esc_html( sprintf( $this->get_text_inline( 'Great, please install cURL and enable it in your php.ini file. In addition, search for the \'disable_functions\' directive in your php.ini file and remove any disabled methods starting with \'curl_\'. To make sure it was successfully activated, use \'phpinfo()\'. Once activated, deactivate the %s and reactivate it back again.', 'curl-missing-sysadmin-desc' ), $this->get_module_label( true ) ) )
),
sprintf(
'%s - %s',
wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . $this->_plugin_basename . '&plugin_status=all&paged=1&s=', 'deactivate-plugin_' . $this->_plugin_basename ),
$deactivate_plugin_title,
$deactivate_plugin_desc
)
)
);
break;
case 'cloudflare_ddos_protection':
$message = sprintf(
$x_requires_access_to_api . ' ' .
$this->esc_html_inline( 'From unknown reason, CloudFlare, the firewall we use, blocks the connection.', 'cloudflare-blocks-connection-message' ) . ' ' .
$happy_to_resolve_issue_asap .
' %s',
'' . $this->get_plugin_name() . '',
sprintf(
'
%s
%s
%s
',
sprintf(
'%s%s',
$fix_issue_title,
' - ' . sprintf(
$fix_issue_desc,
'' . $admin_email . ''
)
),
sprintf(
'%s - %s',
sprintf( 'https://wordpress.org/plugins/%s/download/', $this->_slug ),
$install_previous_title,
$install_previous_desc
),
sprintf(
'%s - %s',
wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . $this->_plugin_basename . '&plugin_status=all&paged=1&s=' . '', 'deactivate-plugin_' . $this->_plugin_basename ),
$deactivate_plugin_title,
$deactivate_plugin_desc
)
)
);
break;
case 'squid_cache_block':
$message = sprintf(
$x_requires_access_to_api . ' ' .
$this->esc_html_inline( 'It looks like your server is using Squid ACL (access control lists), which blocks the connection.', 'squid-blocks-connection-message' ) .
' %s',
'' . $this->get_plugin_name() . '',
sprintf(
'
%s
%s
%s
',
sprintf(
'%s - %s',
$this->esc_html_inline( 'I don\'t know what is Squid or ACL, help me!', 'squid-no-clue-title' ),
sprintf(
$this->esc_html_inline( 'We\'ll make sure to contact your hosting company and resolve the issue. You will get a follow-up email to %s once we have an update.', 'squid-no-clue-desc' ),
'' . $admin_email . ''
)
),
sprintf(
'%s - %s',
$sysadmin_title,
sprintf(
$this->esc_html_inline( 'Great, please whitelist the following domains: %s. Once you are done, deactivate the %s and activate it again.', 'squid-sysadmin-desc' ),
// We use a filter since the plugin might require additional API connectivity.
'' . implode( ', ', $this->apply_filters( 'api_domains', array(
'api.freemius.com',
'wp.freemius.com'
) ) ) . '',
$this->_module_type
)
),
sprintf(
'%s - %s',
wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . $this->_plugin_basename . '&plugin_status=all&paged=1&s=', 'deactivate-plugin_' . $this->_plugin_basename ),
$deactivate_plugin_title,
$deactivate_plugin_desc
)
)
);
break;
// default:
// $message = $this->get_text_inline( 'connectivity-test-fails-message' );
// break;
}
}
$message_id = 'failed_connect_api';
$type = 'error';
$connectivity_test_fails_message = $this->esc_html_inline( 'From unknown reason, the API connectivity test failed.', 'connectivity-test-fails-message' );
if ( false === $message ) {
if ( $is_first_failure ) {
// First attempt failed.
$message = sprintf(
$x_requires_access_to_api . ' ' .
$connectivity_test_fails_message . ' ' .
$this->esc_html_inline( 'It\'s probably a temporary issue on our end. Just to be sure, with your permission, would it be o.k to run another connectivity test?', 'connectivity-test-maybe-temporary' ) . '
',
$this->get_text_inline( 'Please follow these steps to complete the upgrade', 'follow-steps-to-complete-upgrade' ),
( empty( $activate_license_string ) ? '' : $activate_license_string . '
' ) .
$this->get_latest_download_link( sprintf(
/* translators: %s: Plan title */
$this->get_text_inline( 'Download the latest %s version', 'download-latest-x-version' ),
$plan_title
) ),
$deactivation_step,
$this->get_text_inline( 'Upload and activate the downloaded version', 'upload-and-activate' ),
'//bit.ly/upload-wp-' . $this->_module_type . 's',
$this->get_text_inline( 'How to upload and activate?', 'howto-upload-activate' )
);
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*
* @param string $url
* @param array $request
*/
private static function enrich_request_for_debug( &$url, &$request ) {
if ( WP_FS__DEBUG_SDK || isset( $_COOKIE['XDEBUG_SESSION'] ) ) {
$url = add_query_arg( 'XDEBUG_SESSION_START', rand( 0, 9999999 ), $url );
$url = add_query_arg( 'XDEBUG_SESSION', 'PHPSTORM', $url );
$request['cookies'] = array(
new WP_Http_Cookie( array(
'name' => 'XDEBUG_SESSION',
'value' => 'PHPSTORM',
) )
);
}
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*
* @param string $url
* @param array $request
* @param int $success_cache_expiration
* @param int $failure_cache_expiration
* @param bool $maybe_enrich_request_for_debug
*
* @return WP_Error|array
*/
static function safe_remote_post(
&$url,
$request,
$success_cache_expiration = 0,
$failure_cache_expiration = 0,
$maybe_enrich_request_for_debug = true
) {
$should_cache = ($success_cache_expiration + $failure_cache_expiration > 0);
$cache_key = $should_cache ? md5( fs_strip_url_protocol($url) . json_encode( $request ) ) : false;
$response = (!WP_FS__DEBUG_SDK && ( false !== $cache_key )) ?
get_transient( $cache_key ) :
false;
if ( false === $response ) {
if ( $maybe_enrich_request_for_debug ) {
self::enrich_request_for_debug( $url, $request );
}
$response = wp_remote_post( $url, $request );
if ( $response instanceof WP_Error ) {
if ( 'https://' === substr( $url, 0, 8 ) &&
isset( $response->errors ) &&
isset( $response->errors['http_request_failed'] )
) {
$http_error = strtolower( $response->errors['http_request_failed'][0] );
if ( false !== strpos( $http_error, 'ssl' ) ||
false !== strpos( $http_error, 'curl error 35' )
) {
// Failed due to old version of cURL or Open SSL (SSLv3 is not supported by CloudFlare).
$url = 'http://' . substr( $url, 8 );
$request['timeout'] = 15;
$response = wp_remote_post( $url, $request );
}
}
}
if ( false !== $cache_key ) {
set_transient(
$cache_key,
$response,
( ( $response instanceof WP_Error ) ?
$failure_cache_expiration :
$success_cache_expiration )
);
}
}
return $response;
}
/**
* This method is used to enrich the after upgrade notice instructions when the upgraded
* license cannot be activated network wide (license quota isn't large enough).
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*
* @return string
*/
private function get_license_network_activation_notice() {
if ( ! $this->_is_network_active ) {
// Module isn't network level activated.
return '';
}
if ( ! fs_is_network_admin() ) {
// Not network level admin.
return '';
}
if ( get_blog_count() == 1 ) {
// There's only a single site in the network so if there's a context license it was already activated.
return '';
}
if ( ! is_object( $this->_license ) ) {
// No context license.
return '';
}
if ( $this->_license->is_single_site() && 0 < $this->_license->activated ) {
// License was already utilized (this is not 100% the case if all the network is localhost sites and the license can be utilized on unlimited localhost sites).
return '';
}
if ( $this->can_activate_license_on_network( $this->_license ) ) {
// License can be activated on all the network, so probably, the license is already activate on all the network (that's how the after upgrade sync works).
return '';
}
return sprintf(
$this->get_text_inline( '%sClick here%s to choose the sites where you\'d like to activate the license on.', 'network-choose-sites-for-license' ),
'',
''
);
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.2.1.7
*
* @param string $key
*
* @return string
*/
function get_text( $key ) {
return fs_text( $key, $this->_slug );
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.2.3
*
* @param string $text Translatable string.
* @param string $key String key for overrides.
*
* @return string
*/
function get_text_inline( $text, $key = '' ) {
return _fs_text_inline( $text, $key, $this->_slug );
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.2.3
*
* @param string $text Translatable string.
* @param string $context Context information for the translators.
* @param string $key String key for overrides.
*
* @return string
*/
function get_text_x_inline( $text, $context, $key ) {
return _fs_text_x_inline( $text, $context, $key, $this->_slug );
}
/**
* @author Vova Feldman (@svovaf)
* @since 1.2.3
*
* @param string $text Translatable string.
* @param string $key String key for overrides.
*
* @return string
*/
function esc_html_inline( $text, $key ) {
return esc_html( _fs_text_inline( $text, $key, $this->_slug ) );
}
#----------------------------------------------------------------------------------
#region Versioning
#----------------------------------------------------------------------------------
/**
* Check if Freemius in SDK upgrade mode.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*
* @return bool
*/
function is_sdk_upgrade_mode() {
return isset( $this->_storage->sdk_upgrade_mode ) ?
$this->_storage->sdk_upgrade_mode :
false;
}
/**
* Turn SDK upgrade mode off.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*/
function set_sdk_upgrade_complete() {
$this->_storage->sdk_upgrade_mode = false;
}
/**
* Check if plugin upgrade mode.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*
* @return bool
*/
function is_plugin_upgrade_mode() {
return isset( $this->_storage->plugin_upgrade_mode ) ?
$this->_storage->plugin_upgrade_mode :
false;
}
/**
* Turn plugin upgrade mode off.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*/
function set_plugin_upgrade_complete() {
$this->_storage->plugin_upgrade_mode = false;
}
#endregion
#----------------------------------------------------------------------------------
#region Permissions
#----------------------------------------------------------------------------------
/**
* Check if specific permission requested.
*
* @author Vova Feldman (@svovaf)
* @since 1.1.6
*
* @param string $permission
*
* @return bool
*/
function is_permission_requested( $permission ) {
return isset( $this->_permissions[ $permission ] ) && ( true === $this->_permissions[ $permission ] );
}
#endregion
#----------------------------------------------------------------------------------
#region Auto Activation
#----------------------------------------------------------------------------------
/**
* Hints the SDK if running an auto-installation.
*
* @var bool
*/
private $_isAutoInstall = false;
/**
* After upgrade callback to install and auto activate a plugin.
* This code will only be executed on explicit request from the user,
* following the practice Jetpack are using with their theme installations.
*
* @link https://make.wordpress.org/plugins/2017/03/16/clarification-of-guideline-8-executable-code-and-installs/
*
* @author Vova Feldman (@svovaf)
* @since 1.2.1.7
*/
function _install_premium_version_ajax_action() {
$this->_logger->entrance();
$this->check_ajax_referer( 'install_premium_version' );
if ( ! $this->is_registered() ) {
// Not registered.
self::shoot_ajax_failure( array(
'message' => $this->get_text_inline( 'Auto installation only works for opted-in users.', 'auto-install-error-not-opted-in' ),
'code' => 'premium_installed',
) );
}
$plugin_id = fs_request_get( 'target_module_id', $this->get_id() );
if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
// Invalid ID.
self::shoot_ajax_failure( array(
'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
'code' => 'invalid_module_id',
) );
}
if ( $plugin_id == $this->get_id() ) {
if ( $this->is_premium() ) {
// Already using the premium code version.
self::shoot_ajax_failure( array(
'message' => $this->get_text_inline( 'Premium version already active.', 'auto-install-error-premium-activated' ),
'code' => 'premium_installed',
) );
}
if ( ! $this->can_use_premium_code() ) {
// Don't have access to the premium code.
self::shoot_ajax_failure( array(
'message' => $this->get_text_inline( 'You do not have a valid license to access the premium version.', 'auto-install-error-invalid-license' ),
'code' => 'invalid_license',
) );
}
if ( ! $this->has_release_on_freemius() ) {
// Plugin is a serviceware, no premium code version.
self::shoot_ajax_failure( array(
'message' => $this->get_text_inline( 'Plugin is a "Serviceware" which means it does not have a premium code version.', 'auto-install-error-serviceware' ),
'code' => 'premium_version_missing',
) );
}
} else {
$addon = $this->get_addon( $plugin_id );
if ( ! is_object( $addon ) ) {
// Invalid add-on ID.
self::shoot_ajax_failure( array(
'message' => $this->get_text_inline( 'Invalid module ID.', 'auto-install-error-invalid-id' ),
'code' => 'invalid_module_id',
) );
}
if ( $this->is_addon_activated( $plugin_id, true ) ) {
// Premium add-on version is already activated.
self::shoot_ajax_failure( array(
'message' => $this->get_text_inline( 'Premium add-on version already installed.', 'auto-install-error-premium-addon-activated' ),
'code' => 'premium_installed',
) );
}
}
$this->_isAutoInstall = true;
// Try to install and activate.
$updater = FS_Plugin_Updater::instance( $this );
$result = $updater->install_and_activate_plugin( $plugin_id );
if ( is_array( $result ) && ! empty( $result['message'] ) ) {
self::shoot_ajax_failure( array(
'message' => $result['message'],
'code' => $result['code'],
) );
}
self::shoot_ajax_success( $result );
}
/**
* Displays module activation dialog box after a successful upgrade
* where the user explicitly requested to auto download and install
* the premium version.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.1.7
*/
function _add_auto_installation_dialog_box() {
$this->_logger->entrance();
if ( ! $this->is_registered() ) {
// Not registered.
return;
}
$plugin_id = fs_request_get( 'plugin_id', $this->get_id() );
if ( ! FS_Plugin::is_valid_id( $plugin_id ) ) {
// Invalid module ID.
return;
}
if ( $plugin_id == $this->get_id() ) {
if ( $this->is_premium() ) {
// Already using the premium code version.
return;
}
if ( ! $this->can_use_premium_code() ) {
// Don't have access to the premium code.
return;
}
if ( ! $this->has_release_on_freemius() ) {
// Plugin is a serviceware, no premium code version.
return;
}
} else {
$addon = $this->get_addon( $plugin_id );
if ( ! is_object( $addon ) ) {
// Invalid add-on ID.
return;
}
if ( $this->is_addon_activated( $plugin_id, true ) ) {
// Premium add-on version is already activated.
return;
}
}
$vars = array(
'id' => $this->_module_id,
'target_module_id' => $plugin_id,
'slug' => $this->_slug,
);
fs_require_template( 'auto-installation.php', $vars );
}
#endregion
#--------------------------------------------------------------------------------
#region Tabs Integration
#--------------------------------------------------------------------------------
#region Module's Original Tabs
/**
* Inject a JavaScript logic to capture the theme tabs HTML.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*/
function _tabs_capture() {
$this->_logger->entrance();
if ( ! $this->is_theme_settings_page() ||
! $this->is_matching_url( $this->main_menu_url() )
) {
return;
}
$params = array(
'id' => $this->_module_id,
);
fs_require_once_template( 'tabs-capture-js.php', $params );
}
/**
* Cache theme's tabs HTML for a week. The cache will also be set as expired
* after version and type (free/premium) changes, in addition to the week period.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*/
function _store_tabs_ajax_action() {
$this->_logger->entrance();
$this->check_ajax_referer( 'store_tabs' );
// Init filesystem if not yet initiated.
WP_Filesystem();
// Get POST body HTML data.
global $wp_filesystem;
$tabs_html = $wp_filesystem->get_contents( "php://input" );
if ( is_string( $tabs_html ) ) {
$tabs_html = trim( $tabs_html );
}
if ( ! is_string( $tabs_html ) || empty( $tabs_html ) ) {
self::shoot_ajax_failure();
}
$this->_cache->set( 'tabs', $tabs_html, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
self::shoot_ajax_success();
}
/**
* Cache theme's settings page custom styles. The cache will also be set as expired
* after version and type (free/premium) changes, in addition to the week period.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*/
function _store_tabs_styles() {
$this->_logger->entrance();
if ( ! $this->is_theme_settings_page() ||
! $this->is_matching_url( $this->main_menu_url() )
) {
return;
}
$wp_styles = wp_styles();
$theme_styles_url = get_template_directory_uri();
$stylesheets = array();
foreach ( $wp_styles->queue as $handler ) {
if ( fs_starts_with( $handler, 'fs_' ) ) {
// Assume that stylesheets that their handler starts with "fs_" belong to the SDK.
continue;
}
/**
* @var _WP_Dependency $stylesheet
*/
$stylesheet = $wp_styles->registered[ $handler ];
if ( fs_starts_with( $stylesheet->src, $theme_styles_url ) ) {
$stylesheets[] = $stylesheet->src;
}
}
if ( ! empty( $stylesheets ) ) {
$this->_cache->set( 'tabs_stylesheets', $stylesheets, 7 * WP_FS__TIME_24_HOURS_IN_SEC );
}
}
/**
* Check if module's original settings page has any tabs.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*
* @return bool
*/
private function has_tabs() {
return $this->_cache->has( 'tabs' );
}
/**
* Get module's settings page HTML content, starting
* from the beginning of the
element,
* until the tabs HTML (including).
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*
* @return string
*/
private function get_tabs_html() {
$this->_logger->entrance();
return $this->_cache->get( 'tabs' );
}
/**
* Check if page should include tabs.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*
* @return bool
*/
private function should_page_include_tabs() {
if ( ! $this->has_settings_menu() ) {
// Don't add tabs if no settings at all.
return false;
}
if ( ! $this->is_theme() ) {
// Only add tabs to themes for now.
return false;
}
if ( ! $this->has_paid_plan() && ! $this->has_addons() ) {
// Only add tabs to monetizing themes.
return false;
}
if ( ! $this->is_theme_settings_page() ) {
// Only add tabs if browsing one of the theme's setting pages.
return false;
}
if ( $this->is_admin_page( 'pricing' ) && fs_request_get_bool( 'checkout' ) ) {
// Don't add tabs on checkout page, we want to reduce distractions
// as much as possible.
return false;
}
return true;
}
/**
* Add the tabs HTML before the setting's page content and
* enqueue any required stylesheets.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*
* @return bool If tabs were included.
*/
function _add_tabs_before_content() {
$this->_logger->entrance();
if ( ! $this->should_page_include_tabs() ) {
return false;
}
/**
* Enqueue the original stylesheets that are included in the
* theme settings page. That way, if the theme settings has
* some custom _styled_ content above the tabs UI, this
* will make sure that the styling is preserved.
*/
$stylesheets = $this->_cache->get( 'tabs_stylesheets', array() );
if ( is_array( $stylesheets ) ) {
for ( $i = 0, $len = count( $stylesheets ); $i < $len; $i ++ ) {
wp_enqueue_style( "fs_{$this->_module_id}_tabs_{$i}", $stylesheets[ $i ] );
}
}
// Cut closing
tag.
echo substr( trim( $this->get_tabs_html() ), 0, - 6 );
return true;
}
/**
* Add the tabs closing HTML after the setting's page content.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*
* @return bool If tabs closing HTML was included.
*/
function _add_tabs_after_content() {
$this->_logger->entrance();
if ( ! $this->should_page_include_tabs() ) {
return false;
}
echo '';
return true;
}
#endregion
/**
* Add in-page JavaScript to inject the Freemius tabs into
* the module's setting tabs section.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*/
function _add_freemius_tabs() {
$this->_logger->entrance();
if ( ! $this->should_page_include_tabs() ) {
return;
}
$params = array( 'id' => $this->_module_id );
fs_require_once_template( 'tabs.php', $params );
}
#endregion
#--------------------------------------------------------------------------------
#region Customizer Integration for Themes
#--------------------------------------------------------------------------------
/**
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*
* @param WP_Customize_Manager $customizer
*/
function _customizer_register( $customizer ) {
$this->_logger->entrance();
if ( $this->is_pricing_page_visible() ) {
require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-upsell-control.php';
$customizer->add_section( 'freemius_upsell', array(
'title' => '★ ' . $this->get_text_inline( 'View paid features', 'view-paid-features' ),
'priority' => 1,
) );
$customizer->add_setting( 'freemius_upsell', array(
'sanitize_callback' => 'esc_html',
) );
$customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
'fs' => $this,
'section' => 'freemius_upsell',
'priority' => 100,
) ) );
}
if ( $this->is_page_visible( 'contact' ) || $this->is_page_visible( 'support' ) ) {
require_once WP_FS__DIR_INCLUDES . '/customizer/class-fs-customizer-support-section.php';
// Main Documentation Link In Customizer Root.
$customizer->add_section( new FS_Customizer_Support_Section( $customizer, 'freemius_support', array(
'fs' => $this,
'priority' => 1000,
) ) );
}
}
#endregion
/**
* If the theme has a paid version, add some custom
* styling to the theme's premium version (if exists)
* to highlight that it's the premium version of the
* same theme, making it easier for identification
* after the user upgrades and upload it to the site.
*
* @author Vova Feldman (@svovaf)
* @since 1.2.2.7
*/
function _style_premium_theme() {
$this->_logger->entrance();
if ( ! self::is_themes_page() ) {
// Only include in the themes page.
return;
}
if ( ! $this->has_paid_plan() ) {
// Only include if has any paid plans.
return;
}
$params = null;
fs_require_once_template( '/js/jquery.content-change.php', $params );
$params = array(
'slug' => $this->_slug,
'id' => $this->_module_id,
);
fs_require_template( '/js/style-premium-theme.php', $params );
}
/**
* This method will return the absolute URL of the module's local icon.
*
* When you are running your plugin or theme on a **localhost** environment, if the icon
* is not found in the local assets folder, try to fetch the icon URL from Freemius. If not set and
* it's a plugin hosted on WordPress.org, try fetching the icon URL from wordpress.org.
* If an icon is found, this method will automatically attempt to download the icon and store it
* in /freemius/assets/img/{slug}.{png|jpg|gif|svg}.
*
* It's important to mention that this method is NOT phoning home since the developer will deploy
* the product with the local icon in the assets folder. The download process just simplifies
* the process for the developer.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*
* @return string
*/
function get_local_icon_url() {
global $fs_active_plugins;
/**
* @since 1.1.7.5
*/
$local_path = $this->apply_filters( 'plugin_icon', false );
if ( is_string( $local_path ) ) {
$icons = array( $local_path );
} else {
$img_dir = WP_FS__DIR_IMG;
// Locate the main assets folder.
if ( 1 < count( $fs_active_plugins->plugins ) ) {
$plugin_or_theme_img_dir = ( $this->is_plugin() ? WP_PLUGIN_DIR : get_theme_root() );
foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
if ( $data->plugin_path == $this->get_plugin_basename() ) {
$img_dir = $plugin_or_theme_img_dir
. '/'
. str_replace( '../themes/', '', $sdk_path )
. '/assets/img';
break;
}
}
}
// Try to locate the icon in the assets folder.
$icons = glob( fs_normalize_path( $img_dir . "/{$this->_slug}.*" ) );
if ( ! is_array( $icons ) || 0 === count( $icons ) ) {
if ( ! WP_FS__IS_LOCALHOST && $this->is_theme() ) {
$icons = array(
fs_normalize_path( $img_dir . '/theme-icon.png' )
);
} else {
$icon_found = false;
$local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.png" );
if ( ! function_exists( 'get_filesystem_method' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
$have_write_permissions = ( 'direct' === get_filesystem_method( array(), fs_normalize_path( $img_dir ) ) );
/**
* IMPORTANT: THIS CODE WILL NEVER RUN AFTER THE PLUGIN IS IN THE REPO.
*
* This code will only be executed once during the testing
* of the plugin in a local environment. The plugin icon file WILL
* already exist in the assets folder when the plugin is deployed to
* the repository.
*/
if ( WP_FS__IS_LOCALHOST && $have_write_permissions ) {
// Fetch icon from Freemius.
$icon = $this->fetch_remote_icon_url();
// Fetch icon from WordPress.org.
if ( empty( $icon ) && $this->is_plugin() && $this->is_org_repo_compliant() ) {
if ( ! function_exists( 'plugins_api' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
}
$plugin_information = plugins_api( 'plugin_information', array(
'slug' => $this->_slug,
'fields' => array(
'sections' => false,
'tags' => false,
'icons' => true
)
) );
if (
! is_wp_error( $plugin_information )
&& isset( $plugin_information->icons )
&& ! empty( $plugin_information->icons )
) {
/**
* Get the smallest icon.
*
* @author Leo Fajardo (@leorw)
* @since 1.2.2
*/
$icon = end( $plugin_information->icons );
}
}
if ( ! empty( $icon ) ) {
if ( 0 !== strpos( $icon, 'http' ) ) {
$icon = 'http:' . $icon;
}
/**
* Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
*
* @author Leo Fajardo (@leorw)
* @since 1.2.2
*/
$ext = pathinfo( strtok( $icon, '?' ), PATHINFO_EXTENSION );
$local_path = fs_normalize_path( "{$img_dir}/{$this->_slug}.{$ext}" );
// Try to download the icon.
$icon_found = fs_download_image( $icon, $local_path );
}
}
if ( ! $icon_found ) {
// No icons found, fallback to default icon.
if ( $have_write_permissions ) {
// If have write permissions, copy default icon.
copy( fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" ), $local_path );
} else {
// If doesn't have write permissions, use default icon path.
$local_path = fs_normalize_path( $img_dir . "/{$this->_module_type}-icon.png" );
}
}
$icons = array( $local_path );
}
}
}
$icon_dir = dirname( $icons[0] );
return fs_img_url( substr( $icons[0], strlen( $icon_dir ) ), $icon_dir );
}
/**
* Fetch module's extended info.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*
* @return object|mixed
*/
private function fetch_module_info() {
return $this->get_api_plugin_scope()->get( 'info.json', false, WP_FS__TIME_WEEK_IN_SEC );
}
/**
* Fetch module's remote icon URL.
*
* @author Vova Feldman (@svovaf)
* @since 2.0.0
*
* @return string
*/
function fetch_remote_icon_url() {
$info = $this->fetch_module_info();
return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
$info->icon :
'';
}
#--------------------------------------------------------------------------------
#region GDPR
#--------------------------------------------------------------------------------
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*
* @return bool
*/
function fetch_and_store_current_user_gdpr_anonymously() {
$pong = $this->ping( null, true );
if ( ! $this->get_api_plugin_scope()->is_valid_ping( $pong ) ) {
return false;
} else {
FS_GDPR_Manager::instance()->store_is_required( $pong->is_gdpr_required );
return $pong->is_gdpr_required;
}
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*
* @param array $user_plugins
*
* @return string
*/
private function get_gdpr_admin_notice_string( $user_plugins ) {
$this->_logger->entrance();
$addons = self::get_all_addons();
foreach ( $user_plugins as $user_plugin ) {
$has_addons = isset( $addons[ $user_plugin->id ] );
if ( WP_FS__MODULE_TYPE_PLUGIN === $user_plugin->type && ! $has_addons ) {
if ( $this->_module_id == $user_plugin->id ) {
$addons = $this->get_addons();
$has_addons = ( ! empty( $addons ) );
} else {
$plugin_api = FS_Api::instance(
$user_plugin->id,
'plugin',
$user_plugin->id,
$user_plugin->public_key,
! $user_plugin->is_live
);
$addons_result = $plugin_api->get( '/addons.json?enriched=true', true );
if ( $this->is_api_result_object( $addons_result, 'plugins' ) &&
is_array( $addons_result->plugins ) &&
! empty( $addons_result->plugins )
) {
$has_addons = true;
}
}
}
$user_plugin->has_addons = $has_addons;
}
$is_single_parent_product = ( 1 === count( $user_plugins ) );
$multiple_products_text = '';
if ( $is_single_parent_product ) {
$single_parent_product = reset( $user_plugins );
$thank_you = sprintf(
"%s",
$single_parent_product->id,
sprintf(
$single_parent_product->has_addons ?
$this->get_text_inline( 'Thank you so much for using %s and its add-ons!', 'thank-you-for-using-product-and-its-addons' ) :
$this->get_text_inline( 'Thank you so much for using %s!', 'thank-you-for-using-product' ),
sprintf('%s', $single_parent_product->title)
)
);
$already_opted_in = sprintf(
$this->get_text_inline( "You've already opted-in to our usage-tracking, which helps us keep improving the %s.", 'already-opted-in-to-product-usage-tracking' ),
( WP_FS__MODULE_TYPE_THEME === $single_parent_product->type ) ? WP_FS__MODULE_TYPE_THEME : WP_FS__MODULE_TYPE_PLUGIN
);
} else {
$thank_you = $this->get_text_inline( 'Thank you so much for using our products!', 'thank-you-for-using-products' );
$already_opted_in = $this->get_text_inline( "You've already opted-in to our usage-tracking, which helps us keep improving them.", 'already-opted-in-to-products-usage-tracking' );
$products_and_add_ons = '';
foreach ( $user_plugins as $user_plugin ) {
if ( ! empty( $products_and_add_ons ) ) {
$products_and_add_ons .= ', ';
}
if ( ! $user_plugin->has_addons ) {
$products_and_add_ons .= sprintf(
"%s",
$user_plugin->id,
$user_plugin->title
);
} else {
$products_and_add_ons .= sprintf(
"%s",
$user_plugin->id,
sprintf(
$this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
$user_plugin->title
)
);
}
}
$multiple_products_text = sprintf(
"%s: %s",
$this->get_text_inline( 'Products', 'products' ),
$products_and_add_ons
);
}
$actions = sprintf(
'
%s - %s
%s - %s
',
sprintf('', $this->get_text_inline( 'Yes', 'yes' ) ),
$this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
sprintf('', $this->get_text_inline( 'No', 'no' ) ),
sprintf(
$this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
'',
''
)
);
return sprintf(
'%s %s %s',
$thank_you,
$already_opted_in,
sprintf($this->get_text_inline( 'Due to the new %sEU General Data Protection Regulation (GDPR)%s compliance requirements it is required that you provide your explicit consent, again, confirming that you are onboard 🙂', 'due-to-gdpr-compliance-requirements' ), '', '') .
'
' .
'' . $this->get_text_inline( "Please let us know if you'd like us to contact you for security & feature updates, educational content, and occasional offers:", 'contact-for-updates' ) . '' .
$actions .
( $is_single_parent_product ? '' : $multiple_products_text )
);
}
/**
* This method is called for opted-in users to fetch the is_marketing_allowed flag of the user for all the
* plugins and themes they've opted in to.
*
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*
* @param string $user_email
* @param string $license_key
* @param array $plugin_ids
* @param string|null $license_key
*
* @return array|false
*/
private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
$request = array(
'method' => 'POST',
'body' => array(),
'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
);
if ( is_string( $user_email ) ) {
$request['body']['email'] = $user_email;
} else {
$request['body']['license_key'] = $license_key;
}
$result = array();
$url = WP_FS__ADDRESS . '/action/service/user_plugin/';
$total_plugin_ids = count( $plugin_ids );
$plugin_ids_count_per_request = 10;
for ( $i = 1; $i <= $total_plugin_ids; $i += $plugin_ids_count_per_request ) {
$plugin_ids_set = array_slice( $plugin_ids, $i - 1, $plugin_ids_count_per_request );
$request['body']['plugin_ids'] = $plugin_ids_set;
$response = self::safe_remote_post(
$url,
$request,
WP_FS__TIME_24_HOURS_IN_SEC,
WP_FS__TIME_12_HOURS_IN_SEC
);
if ( ! is_wp_error( $response ) ) {
$decoded = is_string( $response['body'] ) ?
json_decode( $response['body'] ) :
null;
if (
!is_object($decoded) ||
!isset($decoded->success) ||
true !== $decoded->success ||
!isset( $decoded->data ) ||
!is_array( $decoded->data )
) {
return false;
}
$result = array_merge( $result, $decoded->data );
}
}
return $result;
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*/
function _maybe_show_gdpr_admin_notice() {
if ( ! $this->is_user_in_admin() ) {
return;
}
if ( ! $this->should_handle_gdpr_admin_notice() ) {
return;
}
if ( ! $this->is_user_admin() ) {
return;
}
require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
$lock = FS_User_Lock::instance();
/**
* Try to acquire a 60-sec lock based on the WP user and thread/process ID.
*/
if ( ! $lock->try_lock( 60 ) ) {
return;
}
/**
* @var $current_wp_user WP_User
*/
$current_wp_user = self::_get_current_wp_user();
/**
* @var FS_User $current_fs_user
*/
$current_fs_user = Freemius::_get_user_by_email( $current_wp_user->user_email );
$ten_years_in_sec = 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC;
if ( ! is_object( $current_fs_user ) ) {
// 10-year lock.
$lock->lock( $ten_years_in_sec );
return;
}
$gdpr = FS_GDPR_Manager::instance();
if ( $gdpr->is_opt_in_notice_shown() ) {
// 30-day lock.
$lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
return;
}
if ( ! $gdpr->should_show_opt_in_notice() ) {
// 10-year lock.
$lock->lock( $ten_years_in_sec );
return;
}
$last_time_notice_shown = $gdpr->last_time_notice_was_shown();
$was_notice_shown_before = ( false !== $last_time_notice_shown );
if ( $was_notice_shown_before &&
30 * WP_FS__TIME_24_HOURS_IN_SEC > time() - $last_time_notice_shown
) {
// If the notice was shown before, show it again after 30 days from the last time it was shown.
return;
}
/**
* Find all plugin IDs that were installed by the current admin.
*/
$plugin_ids_map = self::get_user_opted_in_module_ids_map( $current_fs_user->id );
if ( empty( $plugin_ids_map )) {
$lock->lock( $ten_years_in_sec );
return;
}
$user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
$current_fs_user->email,
null,
array_keys( $plugin_ids_map )
);
if ( empty( $user_plugins ) ) {
$lock->lock(
is_array($user_plugins) ?
$ten_years_in_sec :
// Lock for 24-hours on errors.
WP_FS__TIME_24_HOURS_IN_SEC
);
return;
}
$has_unset_marketing_optin = false;
foreach ( $user_plugins as $user_plugin ) {
if ( true == $user_plugin->is_marketing_allowed ) {
unset( $plugin_ids_map[ $user_plugin->plugin_id ] );
}
if ( ! $has_unset_marketing_optin && is_null( $user_plugin->is_marketing_allowed ) ) {
$has_unset_marketing_optin = true;
}
}
if ( empty( $plugin_ids_map ) ||
( $was_notice_shown_before && ! $has_unset_marketing_optin )
) {
$lock->lock( $ten_years_in_sec );
return;
}
$modules = array_merge(
array_values( self::$_accounts->get_option( 'plugins', array() ) ),
array_values( self::$_accounts->get_option( 'themes', array() ) )
);
foreach ( $modules as $module ) {
if ( ! FS_Plugin::is_valid_id( $module->parent_plugin_id ) && isset( $plugin_ids_map[ $module->id ] ) ) {
$plugin_ids_map[ $module->id ] = $module;
}
}
$plugin_title = null;
if ( 1 === count( $plugin_ids_map ) ) {
$module = reset( $plugin_ids_map );
$plugin_title = $module->title;
}
$gdpr->add_opt_in_sticky_notice(
$this->get_gdpr_admin_notice_string( $plugin_ids_map ),
$plugin_title
);
$this->add_gdpr_optin_ajax_handler_and_style();
$gdpr->notice_was_just_shown();
// 30-day lock.
$lock->lock( 30 * WP_FS__TIME_24_HOURS_IN_SEC );
}
/**
* Prevents the GDPR opt-in admin notice from being added if the user has already chosen to allow or not allow
* marketing.
*
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*/
private function disable_opt_in_notice_and_lock_user() {
FS_GDPR_Manager::instance()->disable_opt_in_notice();
require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
// 10-year lock.
FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*/
function _add_gdpr_optin_js() {
$vars = array( 'id' => $this->_module_id );
fs_require_once_template( 'gdpr-optin-js.php', $vars );
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*/
function enqueue_gdpr_optin_notice_style() {
fs_enqueue_local_style( 'fs_gdpr_optin_notice', '/admin/gdpr-optin-notice.css' );
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*/
function _maybe_add_gdpr_optin_ajax_handler() {
$this->add_ajax_action( 'fetch_is_marketing_required_flag_value', array( &$this, '_fetch_is_marketing_required_flag_value_ajax_action' ) );
if ( FS_GDPR_Manager::instance()->is_opt_in_notice_shown() ) {
$this->add_gdpr_optin_ajax_handler_and_style();
}
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*/
function _fetch_is_marketing_required_flag_value_ajax_action() {
$this->_logger->entrance();
$this->check_ajax_referer( 'fetch_is_marketing_required_flag_value' );
$license_key = fs_request_get( 'license_key' );
if ( empty($license_key) ) {
self::shoot_ajax_failure( $this->get_text_inline( 'License key is empty.', 'empty-license-key' ) );
}
$user_plugins = $this->fetch_user_marketing_flag_status_by_plugins(
null,
$license_key,
array( $this->_module_id )
);
if ( ! is_array( $user_plugins ) ||
empty($user_plugins) ||
!isset($user_plugins[0]->plugin_id) ||
$user_plugins[0]->plugin_id != $this->_module_id
) {
/**
* If faced an error or if the module ID do not match to the current module, ask for GDPR opt-in.
*
* @author Vova Feldman (@svovaf)
*/
self::shoot_ajax_success( array( 'is_marketing_allowed' => null ) );
}
self::shoot_ajax_success( array( 'is_marketing_allowed' => $user_plugins[0]->is_marketing_allowed ) );
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*/
private function add_gdpr_optin_ajax_handler_and_style() {
// Add GDPR action AJAX callback.
$this->add_ajax_action( 'gdpr_optin_action', array( &$this, '_gdpr_optin_ajax_action' ) );
add_action( 'admin_footer', array( &$this, '_add_gdpr_optin_js' ) );
add_action( 'admin_enqueue_scripts', array( &$this, 'enqueue_gdpr_optin_notice_style' ) );
}
/**
* @author Leo Fajardo (@leorw)
* @since 2.1.0
*/
function _gdpr_optin_ajax_action() {
$this->_logger->entrance();
$this->check_ajax_referer( 'gdpr_optin_action' );
if ( ! fs_request_has( 'is_marketing_allowed' ) || ! fs_request_has( 'plugin_ids' ) ) {
self::shoot_ajax_failure();
}
$current_wp_user = self::_get_current_wp_user();
$plugin_ids = fs_request_get( 'plugin_ids', array() );
if ( ! is_array( $plugin_ids ) || empty( $plugin_ids ) ) {
self::shoot_ajax_failure();
}
$modules = array_merge(
array_values( self::$_accounts->get_option( 'plugins', array() ) ),
array_values( self::$_accounts->get_option( 'themes', array() ) )
);
foreach ( $modules as $key => $module ) {
if ( ! in_array( $module->id, $plugin_ids ) ) {
unset( $modules[ $key ] );
}
}
if ( empty( $modules ) ) {
self::shoot_ajax_failure();
}
$user_api = $this->get_api_user_scope_by_user( Freemius::_get_user_by_email( $current_wp_user->user_email ) );
foreach ( $modules as $module ) {
$user_api->call( "?plugin_id={$module->id}", 'put', array(
'is_marketing_allowed' => ( true == fs_request_get_bool( 'is_marketing_allowed' ) )
) );
}
FS_GDPR_Manager::instance()->remove_opt_in_notice();
require_once WP_FS__DIR_INCLUDES . '/class-fs-user-lock.php';
// 10-year lock.
FS_User_Lock::instance()->lock( 10 * 365 * WP_FS__TIME_24_HOURS_IN_SEC );
self::shoot_ajax_success();
}
/**
* Checks if the GDPR admin notice should be handled. By default, this logic is off, unless the integrator adds the special 'handle_gdpr_admin_notice' filter.
*
* @author Vova Feldman (@svovaf)
* @since 2.1.0
*
* @return bool
*/
private function should_handle_gdpr_admin_notice() {
return $this->apply_filters(
'handle_gdpr_admin_notice',
// Default to false.
false
);
}
#endregion
#----------------------------------------------------------------------------------
#region Marketing
#----------------------------------------------------------------------------------
/**
* Check if current user purchased any other plugins before.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*
* @return bool
*/
function has_purchased_before() {
// TODO: Implement has_purchased_before() method.
throw new Exception( 'not implemented' );
}
/**
* Check if current user classified as an agency.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*
* @return bool
*/
function is_agency() {
// TODO: Implement is_agency() method.
throw new Exception( 'not implemented' );
}
/**
* Check if current user classified as a developer.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*
* @return bool
*/
function is_developer() {
// TODO: Implement is_developer() method.
throw new Exception( 'not implemented' );
}
/**
* Check if current user classified as a business.
*
* @author Vova Feldman (@svovaf)
* @since 1.0.9
*
* @return bool
*/
function is_business() {
// TODO: Implement is_business() method.
throw new Exception( 'not implemented' );
}
#endregion
#----------------------------------------------------------------------------------
#region Helper
#----------------------------------------------------------------------------------
/**
* If running with a secret key, assume it's the developer and show pending plans as well.
*
* @author Vova Feldman (@svovaf)
* @since 2.1.2
*
* @param string $path
*
* @return string
*/
function add_show_pending( $path ) {
if ( ! $this->has_secret_key() ) {
return $path;
}
return $path . ( false !== strpos( $path, '?' ) ? '&' : '?' ) . 'show_pending=true';
}
#endregion
}