diff --git a/includes/class-freemius.php b/includes/class-freemius.php index cf9c9b70..1c8c63a4 100755 --- a/includes/class-freemius.php +++ b/includes/class-freemius.php @@ -3692,12 +3692,14 @@ static function _load_textdomain() { global $fs_active_plugins; - // Works both for plugins and themes. - load_plugin_textdomain( - 'freemius', - false, - $fs_active_plugins->newest->sdk_path . '/languages/' - ); + // Defensive guard: $fs_active_plugins may not be initialized yet in deferred load scenarios. + if ( ! is_object( $fs_active_plugins ) || + ! isset( $fs_active_plugins->newest ) || + ! is_object( $fs_active_plugins->newest ) || + empty( $fs_active_plugins->newest->sdk_path ) ) { + return; + } + } #endregion diff --git a/includes/managers/class-fs-debug-manager.php b/includes/managers/class-fs-debug-manager.php index 5364712d..df5cba4b 100644 --- a/includes/managers/class-fs-debug-manager.php +++ b/includes/managers/class-fs-debug-manager.php @@ -16,8 +16,9 @@ class FS_DebugManager { * @since 1.0.8 */ static function _add_debug_section() { - if ( ! is_super_admin() ) { - // Add debug page only for super-admins. + // Allow only proper admins: single-site admins or network admins in multisite. + $capability = fs_is_network_admin() ? 'manage_network_options' : 'manage_options'; + if ( ! current_user_can( $capability ) ) { return; } @@ -30,20 +31,25 @@ static function _add_debug_section() { $hook = FS_Admin_Menu_Manager::add_page( $title, $title, - 'manage_options', + $capability, 'freemius', array( self::class, '_debug_page_render' ) ); } else { - // Add hidden debug page. - $hook = FS_Admin_Menu_Manager::add_subpage( - '', - $title, - $title, - 'manage_options', + // Register as a top-level page and immediately hide it from the menu. + // This guarantees a valid screen + title while keeping the page undiscoverable via UI. + $hook = FS_Admin_Menu_Manager::add_page( + $title, // page title + $title, // menu title (will be hidden) + $capability, 'freemius', array( self::class, '_debug_page_render' ) ); + if ( ! empty( $hook ) ) { + add_action( ( fs_is_network_admin() ? 'network_' : '' ) . 'admin_menu', function () { + remove_menu_page( 'freemius' ); + }, 999 ); + } } if ( ! empty( $hook ) ) { @@ -472,6 +478,8 @@ public static function load_required_static() { self::class, '_add_debug_section', ) ); + // Ensure $title is never null after current screen is set (prevents PHP 8.1+ strip_tags(null) deprecation). + add_action( 'current_screen', array( self::class, '_ensure_admin_title_on_current_screen' ), 1, 1 ); } add_action( "wp_ajax_fs_toggle_debug_mode", array( self::class, '_toggle_debug_mode' ) ); @@ -492,6 +500,27 @@ public static function register_hooks() { add_action( 'fs_debug_turn_off_logging_hook', array( self::class, '_turn_off_debug_mode' ) ); } + /** + * Ensure the global $title is a string once the current screen exists. + * This prevents core admin-header.php from calling strip_tags(null) on PHP 8.1+. + * + * @param WP_Screen $screen + * @since 2.12.2.3 + */ + public static function _ensure_admin_title_on_current_screen( $screen ) { + // $title is a core global used by admin-header.php. + global $title; + if ( null === $title ) { + // Try to derive a proper title from the screen; otherwise default to empty string. + if ( is_object( $screen ) && method_exists( $screen, 'get_title' ) ) { + $derived = $screen->get_title(); + $title = is_string( $derived ) ? $derived : ''; + } else { + $title = ''; + } + } + } + /** * @author Daniele Alessandra (@danielealessandra) * diff --git a/start.php b/start.php index 5f1b1901..c31de3ae 100644 --- a/start.php +++ b/start.php @@ -10,12 +10,133 @@ return; } - /** - * Freemius SDK Version. - * - * @var string - */ - $this_sdk_version = '2.12.2'; + /** + * Freemius SDK Version. + * + * @var string + */ + $this_sdk_version = '2.12.2'; + + // Define SDK version constant once, early, so it's available for deferred paths too. + if ( ! defined( 'WP_FS__SDK_VERSION' ) ) { + define( 'WP_FS__SDK_VERSION', $this_sdk_version ); + } + + /** + * Allow skipping the early "latest SDK loader" but still support safe, late initialization. + * + * When FS__SKIP_LATEST_SDK_LOADING is true, we avoid the early loader that may run before WP is fully bootstrapped + * (common in Bedrock/Altis when Composer autoload runs too early). Instead of disabling the SDK, we provide a + * deferring shim for fs_dynamic_init() that initializes the SDK as soon as the WordPress plugin API is available. + * + * @since 2.12.2.2 + */ + if ( defined( 'FS__SKIP_LATEST_SDK_LOADING' ) && FS__SKIP_LATEST_SDK_LOADING ) { + // Queue for modules waiting for late init. + if ( ! isset( $GLOBALS['fs__deferred_modules'] ) || ! is_array( $GLOBALS['fs__deferred_modules'] ) ) { + $GLOBALS['fs__deferred_modules'] = array(); + } + + if ( ! function_exists( 'fs__process_deferred_freemius_inits' ) ) { + /** + * Process deferred Freemius initializations. + * Loads the SDK core and initializes all queued modules. + */ + function fs__process_deferred_freemius_inits() { + if ( class_exists( 'Freemius' ) ) { + // SDK already loaded. + } else { + // Load SDK core files directly, bypassing start.php to avoid recursion/early loader. + // Preload essentials to satisfy functions used by config.php (e.g., fs_get_ip()). + $fs_sdk_dir = dirname( __FILE__ ); + $fs_essential = $fs_sdk_dir . '/includes/fs-essential-functions.php'; + if ( file_exists( $fs_essential ) ) { + require_once $fs_essential; + } + $fs_ip = $fs_sdk_dir . '/includes/fs-ip.php'; + if ( file_exists( $fs_ip ) ) { + require_once $fs_ip; + } + require_once $fs_sdk_dir . '/require.php'; + } + + if ( isset( $GLOBALS['fs__deferred_modules'] ) && is_array( $GLOBALS['fs__deferred_modules'] ) ) { + foreach ( $GLOBALS['fs__deferred_modules'] as $module ) { + if ( is_array( $module ) && isset( $module['id'], $module['slug'] ) ) { + try { + $fs = Freemius::instance( $module['id'], $module['slug'], true ); + $fs->dynamic_init( $module ); + } catch ( Exception $e ) { + // Swallow to avoid breaking host; log if WP_DEBUG is true. + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + error_log( '[Freemius] Deferred init failed: ' . $e->getMessage() ); + } + } + } + } + // Clear the queue. + $GLOBALS['fs__deferred_modules'] = array(); + } + } + } + + if ( ! function_exists( 'fs_dynamic_init' ) ) { + /** + * Deferring shim for fs_dynamic_init(). + * + * If the WordPress plugin API is already available, initialize immediately. + * Otherwise, queue the module and ensure processing happens on 'plugins_loaded'. + * + * @param array $module + * @return Freemius|null + */ + function fs_dynamic_init( $module = array() ) { + // If WP plugin API is ready, initialize now. + if ( function_exists( 'add_action' ) ) { + if ( ! class_exists( 'Freemius' ) ) { + // Preload essentials to satisfy functions used by config.php (e.g., fs_get_ip()). + $fs_sdk_dir = dirname( __FILE__ ); + $fs_essential = $fs_sdk_dir . '/includes/fs-essential-functions.php'; + if ( file_exists( $fs_essential ) ) { + require_once $fs_essential; + } + $fs_ip = $fs_sdk_dir . '/includes/fs-ip.php'; + if ( file_exists( $fs_ip ) ) { + require_once $fs_ip; + } + require_once $fs_sdk_dir . '/require.php'; + } + if ( class_exists( 'Freemius' ) && is_array( $module ) && isset( $module['id'], $module['slug'] ) ) { + $fs = Freemius::instance( $module['id'], $module['slug'], true ); + $fs->dynamic_init( $module ); + return $fs; + } + } + + // Otherwise, defer until plugins_loaded. + if ( ! isset( $GLOBALS['fs__deferred_modules'] ) || ! is_array( $GLOBALS['fs__deferred_modules'] ) ) { + $GLOBALS['fs__deferred_modules'] = array(); + } + $GLOBALS['fs__deferred_modules'][] = $module; + + // If WP hooks are available, schedule processing (idempotent). + if ( function_exists( 'add_action' ) ) { + add_action( 'plugins_loaded', 'fs__process_deferred_freemius_inits', 1 ); + } + + return null; + } + } + + // If WP hooks are already available at this point (e.g., start.php was included late), + // schedule immediate processing of any queued modules. + if ( function_exists( 'add_action' ) ) { + add_action( 'plugins_loaded', 'fs__process_deferred_freemius_inits', 1 ); + } + + // Important: return to skip the early loader below. + return; + } #region SDK Selection Logic -------------------------------------------------------------------- diff --git a/templates/debug.php b/templates/debug.php index f620bfd5..ff08d184 100644 --- a/templates/debug.php +++ b/templates/debug.php @@ -12,6 +12,18 @@ global $fs_active_plugins; + // Derive SDK version from the constant only. Avoid referencing $fs_active_plugins here because it may be null + // during deferred initialization and we only need a display value for the heading. + $sdk_version = defined('WP_FS__SDK_VERSION') ? (string) WP_FS__SDK_VERSION : ''; + + // Normalize plugins list for the SDK versions table. + $fs_plugins = array(); + if ( is_object( $fs_active_plugins ) + && isset( $fs_active_plugins->plugins ) + && is_array( $fs_active_plugins->plugins ) ) { + $fs_plugins = $fs_active_plugins->plugins; + } + $fs_options = FS_Options::instance( WP_FS__ACCOUNTS_OPTION_NAME, true ); $off_text = fs_text_x_inline( 'Off', 'as turned off' ); @@ -26,7 +38,7 @@ $auto_off_timestamp = wp_next_scheduled( 'fs_debug_turn_off_logging_hook' ) * 1000; ?> -

newest->version ?>

+

@@ -304,7 +316,7 @@ function stopCountdownManually() { - plugins as $sdk_path => $data ) : ?> + $data ) : ?> version ) ?> id ); - - $active_modules_by_id[ $data->id ] = true; + if ( function_exists( 'freemius' ) ) { + $fs = freemius( $data->id ); + } elseif ( class_exists( 'Freemius' ) ) { + // Fallback for deferred loads where the helper function may not be defined yet. + // We have $slug from the loop key. + $fs = Freemius::instance( $data->id, $slug, true ); + } + if ( is_object( $fs ) ) { + $active_modules_by_id[ $data->id ] = true; + } } ?> has_api_connectivity(); + $has_api_connectivity = is_object( $fs ) ? $fs->has_api_connectivity() : null; - if ( true === $has_api_connectivity && $fs->is_on() ) { + if ( true === $has_api_connectivity && is_object( $fs ) && $fs->is_on() ) { echo ' style="background: #E6FFE6; font-weight: bold"'; - } else if ( false === $has_api_connectivity || ! $fs->is_on() ) { + } else if ( false === $has_api_connectivity || ( is_object( $fs ) && ! $fs->is_on() ) ) { echo ' style="background: #ffd0d0; font-weight: bold"'; } } ?>> @@ -400,13 +419,11 @@ function stopCountdownManually() { fs_text_x_inline( 'No requests yet', 'API connectivity state is unknown' ) ) ); } ?> - is_on() ) { + is_on() ) { echo ' style="color: red; text-transform: uppercase;"'; } ?>>is_on() ? - $on_text : - $off_text - ); + $is_on = ( is_object( $fs ) && $fs->is_on() ); + echo esc_html( $is_on ? $on_text : $off_text ); } ?> file ?> public_key ?> @@ -427,7 +444,7 @@ function stopCountdownManually() { - has_trial_plan() ) : ?> + has_trial_plan() ) : ?>
@@ -436,10 +453,10 @@ function stopCountdownManually() {
- is_registered() ) : ?> + is_registered() ) : ?> - is_network_upgrade_mode() ) : ?> + is_network_upgrade_mode() ) : ?>
@@ -948,4 +965,4 @@ class="dashicons dashicons-download"> \ No newline at end of file + diff --git a/templates/debug/scheduled-crons.php b/templates/debug/scheduled-crons.php index 0f5248e3..aea3db49 100644 --- a/templates/debug/scheduled-crons.php +++ b/templates/debug/scheduled-crons.php @@ -37,36 +37,42 @@ * @since 1.2.1 Don't load data from inactive modules. */ if ( $is_active ) { - $fs = freemius( $data->id ); + $fs = null; + // Obtain the Freemius instance safely in deferred-load scenarios. + if ( function_exists( 'freemius' ) ) { + $fs = freemius( $data->id ); + } elseif ( class_exists( 'Freemius' ) ) { + $fs = Freemius::instance( $data->id, $slug, true ); + } - $next_execution = $fs->next_sync_cron(); - $last_execution = $fs->last_sync_cron(); + if ( is_object( $fs ) ) { + $next_execution = $fs->next_sync_cron(); + $last_execution = $fs->last_sync_cron(); - if ( false !== $next_execution ) { - $scheduled_crons[ $slug ][] = array( - 'name' => $fs->get_plugin_name(), - 'slug' => $slug, - 'module_type' => $fs->get_module_type(), - 'type' => 'sync_cron', - 'last' => $last_execution, - 'next' => $next_execution, - ); - } + if ( false !== $next_execution ) { + $scheduled_crons[ $slug ][] = array( + 'name' => $fs->get_plugin_name(), + 'slug' => $slug, + 'module_type' => $fs->get_module_type(), + 'type' => 'sync_cron', + 'last' => $last_execution, + 'next' => $next_execution, + ); + } - $next_install_execution = $fs->next_install_sync(); - $last_install_execution = $fs->last_install_sync(); + $next_install_execution = $fs->next_install_sync(); + $last_install_execution = $fs->last_install_sync(); - if (false !== $next_install_execution || - false !== $last_install_execution - ) { - $scheduled_crons[ $slug ][] = array( - 'name' => $fs->get_plugin_name(), - 'slug' => $slug, - 'module_type' => $fs->get_module_type(), - 'type' => 'install_sync', - 'last' => $last_install_execution, - 'next' => $next_install_execution, - ); + if ( false !== $next_install_execution || false !== $last_install_execution ) { + $scheduled_crons[ $slug ][] = array( + 'name' => $fs->get_plugin_name(), + 'slug' => $slug, + 'module_type' => $fs->get_module_type(), + 'type' => 'install_sync', + 'last' => $last_install_execution, + 'next' => $next_install_execution, + ); + } } } } diff --git a/templates/forms/deactivation/form.php b/templates/forms/deactivation/form.php index 3ade285f..6bd2509a 100755 --- a/templates/forms/deactivation/form.php +++ b/templates/forms/deactivation/form.php @@ -12,8 +12,21 @@ /** * @var array $VARS + * @var Freemius|null $fs */ - $fs = freemius( $VARS['id'] ); + $fs = null; + if ( function_exists( 'freemius' ) ) { + $fs = freemius( $VARS['id'] ); + } elseif ( class_exists( 'Freemius' ) && method_exists( 'Freemius', 'get_instance_by_id' ) ) { + // Fallback for deferred-load scenarios where the helper function may not be defined yet. + $fs = Freemius::get_instance_by_id( $VARS['id'] ); + } + + // Abort rendering if the Freemius instance is unavailable (fail-safe). + if ( ! is_object( $fs ) ) { + return; + } + $slug = $fs->get_slug(); $subscription_cancellation_dialog_box_template_params = $VARS['subscription_cancellation_dialog_box_template_params']; diff --git a/templates/forms/deactivation/retry-skip.php b/templates/forms/deactivation/retry-skip.php index 17321029..f6b6fe5c 100644 --- a/templates/forms/deactivation/retry-skip.php +++ b/templates/forms/deactivation/retry-skip.php @@ -12,8 +12,21 @@ /** * @var array $VARS + * @var Freemius|null $fs */ - $fs = freemius( $VARS['id'] ); + $fs = null; + if ( function_exists( 'freemius' ) ) { + $fs = freemius( $VARS['id'] ); + } elseif ( class_exists( 'Freemius' ) && method_exists( 'Freemius', 'get_instance_by_id' ) ) { + // Fallback for deferred loads where the helper function may not be defined yet. + $fs = Freemius::get_instance_by_id( $VARS['id'] ); + } + + // If we couldn't resolve the Freemius instance, abort rendering safely. + if ( ! is_object( $fs ) ) { + return; + } + $slug = $fs->get_slug(); $skip_url = fs_nonce_url( $fs->_get_admin_page_url( '', array( 'fs_action' => $fs->get_unique_affix() . '_skip_activation' ) ), $fs->get_unique_affix() . '_skip_activation' ); @@ -21,4 +34,4 @@ $use_plugin_anonymously_text = fs_text_inline( 'Click here to use the plugin anonymously', 'click-here-to-use-plugin-anonymously', $slug ); echo sprintf( fs_text_inline( "You might have missed it, but you don't have to share any data and can just %s the opt-in.", 'dont-have-to-share-any-data', $slug ), "{$skip_text}" ) - . " {$use_plugin_anonymously_text}"; \ No newline at end of file + . " {$use_plugin_anonymously_text}"; diff --git a/templates/forms/optout.php b/templates/forms/optout.php index 8d890142..d94b7e72 100644 --- a/templates/forms/optout.php +++ b/templates/forms/optout.php @@ -12,9 +12,24 @@ /** * @var array $VARS - * @var Freemius $fs + * @var Freemius|null $fs */ - $fs = freemius( $VARS['id'] ); + $fs = null; + if ( function_exists( 'freemius' ) ) { + $fs = freemius( $VARS['id'] ); + } elseif ( class_exists( 'Freemius' ) ) { + // Attempt to reuse an existing instance by ID if available; fall back to a no-op if unresolved. + // Some SDK versions expose a static getter; if not, the following will be harmless. + if ( method_exists( 'Freemius', 'get_instance_by_id' ) ) { + $fs = Freemius::get_instance_by_id( $VARS['id'] ); + } + } + + // If we couldn't resolve the Freemius instance, abort rendering the opt-out modal safely. + if ( ! is_object( $fs ) ) { + return; + } + $slug = $fs->get_slug(); $reconnect_url = $fs->get_activation_url( array(