: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* Add in-page JavaScript to inject the Freemius tabs into
* the module's setting tabs section.
* @author Vova Feldman (@svovaf)
function _add_freemius_tabs() {
$this->_logger->entrance();
if ( ! $this->should_page_include_tabs() ) {
$params = array( 'id' => $this->_module_id );
fs_require_once_template( 'tabs.php', $params );
#--------------------------------------------------------------------------------
#region Customizer Integration for Themes
#--------------------------------------------------------------------------------
* @author Vova Feldman (@svovaf)
* @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' ),
$customizer->add_setting( 'freemius_upsell', array(
'sanitize_callback' => 'esc_html',
$customizer->add_control( new FS_Customizer_Upsell_Control( $customizer, 'freemius_upsell', array(
'section' => 'freemius_upsell',
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(
* 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)
function _style_premium_theme() {
$this->_logger->entrance();
if ( ! self::is_themes_page() ) {
// Only include in the themes page.
if ( ! $this->has_paid_plan() ) {
// Only include if has any paid plans.
fs_require_once_template( '/js/jquery.content-change.php', $params );
'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)
function get_local_icon_url() {
global $fs_active_plugins;
$local_path = $this->apply_filters( 'plugin_icon', false );
if ( is_string( $local_path ) ) {
$icons = array( $local_path );
$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( get_stylesheet() ) );
foreach ( $fs_active_plugins->plugins as $sdk_path => &$data ) {
if ( $data->plugin_path == $this->get_plugin_basename() ) {
$img_dir = $plugin_or_theme_img_dir
* The basename will be `themes` or the basename of a custom themes directory.
* @author Leo Fajardo (@leorw)
. str_replace( '../' . basename( $plugin_or_theme_img_dir ) . '/', '', $sdk_path )
// 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() ) {
fs_normalize_path( $img_dir . '/theme-icon.png' )
$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
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(
! is_wp_error( $plugin_information )
&& isset( $plugin_information->icons )
&& ! empty( $plugin_information->icons )
* @author Leo Fajardo (@leorw)
$icon = end( $plugin_information->icons );
if ( ! empty( $icon ) ) {
if ( 0 !== strpos( $icon, 'http' ) ) {
* Get a clean file extension, e.g.: "jpg" and not "jpg?rev=1305765".
* @author Leo Fajardo (@leorw)
$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 );
// 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 );
// 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)
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)
function fetch_remote_icon_url() {
$info = $this->fetch_module_info();
return ( $this->is_api_result_object( $info, 'icon' ) && is_string( $info->icon ) ) ?
#--------------------------------------------------------------------------------
#--------------------------------------------------------------------------------
* @author Leo Fajardo (@leorw)
* @param array $user_plugins
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 ) );
$plugin_api = FS_Api::instance(
$user_plugin->public_key,
$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 )
$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 );
"<span data-plugin-id='%d'>%s</span>",
$single_parent_product->id,
$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('<b><i>%s</i></b>', $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
$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(
"<span data-plugin-id='%d'>%s</span>",
$products_and_add_ons .= sprintf(
"<span data-plugin-id='%d'>%s</span>",
$this->get_text_inline( '%s and its add-ons', 'product-and-its-addons' ),
$multiple_products_text = sprintf(
"<small class='products'><strong>%s:</strong> %s</small>",
$this->get_text_inline( 'Products', 'products' ),
'<ul><li>%s<span class="action-description"> - %s</span></li><li>%s<span class="action-description"> - %s</span></li></ul>',
sprintf('<button class="button button-primary allow-marketing">%s</button>', $this->get_text_inline( 'Yes', 'yes' ) ),
$this->get_text_inline( 'send me security & feature updates, educational content and offers.', 'send-updates' ),
sprintf('<button class="button button-secondary">%s</button>', $this->get_text_inline( 'No', 'no' ) ),
$this->get_text_inline( 'do %sNOT%s send me security & feature updates, educational content and offers.', 'do-not-send-updates' ),
'<span class="underlined">',
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' ), '<a href="https://ec.europa.eu/info/law/law-topic/data-protection_en/" target="_blank" rel="noopener noreferrer">', '</a>' ) .
'<b>' . $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' ) . '</b>' .
( $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)
* @param string $user_email
* @param string $license_key
* @param array $plugin_ids
* @param string|null $license_key
private function fetch_user_marketing_flag_status_by_plugins( $user_email, $license_key, $plugin_ids ) {
'timeout' => WP_FS__DEBUG_SDK ? 60 : 30,
if ( is_string( $user_email ) ) {
$request['body']['email'] = $user_email;
$request['body']['license_key'] = $license_key;
$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(
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'] ) :
!isset($decoded->success) ||
true !== $decoded->success ||
!isset( $decoded->data ) ||
!is_array( $decoded->data )
$result = array_merge( $result, $decoded->data );
* @author Leo Fajardo (@leorw)
function _maybe_show_gdpr_admin_notice() {
if ( ! $this->is_user_in_admin() ) {
if ( ! $this->should_handle_gdpr_admin_notice() ) {
if ( ! $this->is_user_admin() ) {