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; ?> -