prefix = $prefix; $this->mainfile = $mainfile; $this->domain = $domain; $this->isPro = $isPro; $this->disableReview = $disableReview; if ( is_admin() ) { // Skip AJAX and REST requests to avoid unnecessary processing if ( MeowKit_MWCODE_Helpers::is_asynchronous_request() ) { return; } // Check if WordPress pluggable functions are available yet. // These are defined in wp-includes/pluggable.php, which WordPress loads // AFTER the 'plugins_loaded' hook but BEFORE the 'init' hook. if ( !function_exists( 'current_user_can' ) || !function_exists( 'wp_get_current_user' ) ) { // Functions don't exist yet - defer admin setup until 'init' hook // This is NORMAL behavior when plugins instantiate on 'plugins_loaded' $this->defer_admin_setup(); // Continue to rest of constructor (filters, license checks, etc.) } else { // Functions already exist - safe to run admin setup immediately // This happens when plugins instantiate on 'init' or later $this->run_admin_setup(); } // License-related admin notices (doesn't require pluggable functions) $license = get_option( $this->prefix . '_license', '' ); if ( !empty( $license ) && !$this->isPro ) { add_action( 'admin_notices', [ $this, 'admin_notices_licensed_free' ] ); } } // ALWAYS register these filters (they work at any time) add_filter( 'plugin_row_meta', [ $this, 'custom_plugin_row_meta' ], 10, 2 ); add_filter( 'edd_sl_api_request_verify_ssl', [ $this, 'request_verify_ssl' ], 10, 0 ); } /** * Defer admin setup until WordPress 'init' hook. * * This method stores the current instance and registers a one-time * 'init' hook callback that will process all deferred instances. * * Why defer? Because we need current_user_can() to check permissions, * and that function doesn't exist until after 'plugins_loaded'. */ private function defer_admin_setup() { // Add this instance to the queue for processing on 'init' self::$deferred_instances[] = $this; // Register the 'init' hook only once (for the first deferred instance) if ( count( self::$deferred_instances ) === 1 ) { add_action( 'init', array( __CLASS__, 'process_deferred_instances' ) ); } } /** * Static callback for 'init' hook - processes all deferred instances. * * By the time 'init' fires, WordPress has loaded pluggable.php and * current_user_can() is guaranteed to exist. We process all instances * that were created during 'plugins_loaded' or earlier. * * This is called as a static method because it processes multiple instances. */ public static function process_deferred_instances() { // Belt-and-suspenders check: pluggable functions should ALWAYS exist by 'init' // If they somehow don't, log a warning and bail (this should never happen) if ( !function_exists( 'current_user_can' ) || !function_exists( 'wp_get_current_user' ) ) { trigger_error( 'MeowKit_MWCODE_Admin: Pluggable functions still unavailable on init hook. ' . 'This should never happen and indicates a serious WordPress core issue.', E_USER_WARNING ); return; } // Process each deferred instance's admin setup foreach ( self::$deferred_instances as $instance ) { $instance->run_admin_setup(); } // Clear the array to free memory (we won't need these references anymore) self::$deferred_instances = array(); } /** * Run admin setup - both shared (once) and per-instance (each plugin). * * SHARED SETUP (once for all plugins): * - Issues detection * - Meow Apps menu creation * - Admin footer customization * * PER-INSTANCE SETUP (once per plugin): * - Ratings system * - News system * * This method is called either immediately (if pluggable functions exist) * or deferred until 'init' (if they don't). Either way, it's safe to call * current_user_can() here. */ private function run_admin_setup() { // SHARED SETUP: Only run once for all Meow Apps plugins if ( !MeowKit_MWCODE_Admin::$loaded ) { // Check for potential issues with WordPress install, other plugins, etc. new MeowKit_MWCODE_Issues( $this->prefix, $this->mainfile, $this->domain ); // Create the unified Meow Apps menu (priority 5 to ensure early creation) add_action( 'admin_menu', [ $this, 'admin_menu_start' ], 5 ); // Customize admin footer on Meow Apps pages $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : null; if ( $page === 'meowapps-main-menu' ) { add_filter( 'admin_footer_text', [ $this, 'admin_footer_text' ], 100000, 1 ); } // Promote AI Engine on the WordPress 7 Connectors page when AI Engine // itself isn't installed. When AI Engine is active, its own banner // takes over — so this path only runs on "bare" Meow Apps installs. add_action( 'admin_footer', [ $this, 'maybe_render_wpai_promo' ] ); MeowKit_MWCODE_Admin::$loaded = true; } // PER-INSTANCE SETUP: Run for each plugin that uses this library // Only admins get ratings prompts and news if ( $this->is_user_admin() ) { if ( !$this->disableReview ) { new MeowKit_MWCODE_Ratings( $this->prefix, $this->mainfile, $this->domain ); } new MeowKit_MWCODE_News( $this->domain ); } } /** * Check if current user is a site administrator. * * This method is only called from run_admin_setup(), which guarantees * that pluggable functions exist. No error logging needed - if the * functions don't exist, we simply return false as a defensive fallback. * * @return bool True if user can manage options, false otherwise */ public function is_user_admin() { // Defensive check (should never fail if called from run_admin_setup) if ( !function_exists( 'current_user_can' ) || !function_exists( 'wp_get_current_user' ) ) { return false; } return current_user_can( 'manage_options' ); } public function custom_plugin_row_meta( $links, $file ) { $path = pathinfo( $file ); $pathName = basename( $path['dirname'] ); $thisPath = pathinfo( $this->mainfile ); $thisPathName = basename( $thisPath['dirname'] ); $isActive = is_plugin_active( $file ); if ( !$isActive ) { return $links; } $isIssue = $this->isPro && !$this->is_registered(); if ( strpos( $pathName, $thisPathName ) !== false ) { // In network admin, handle differently (no settings page available) if ( is_network_admin() ) { if ( $this->isPro && !$this->is_registered() ) { // Show "Register License" link for unregistered Pro plugins $new_links = [ 'license' => sprintf( '%s', esc_attr( $this->prefix ), esc_attr( $this->nice_name_from_file( $this->mainfile ) ), __( 'Register License', $this->domain ) ), ]; // Track this plugin for the modal self::$network_license_plugins[ $this->prefix ] = $this->nice_name_from_file( $this->mainfile ); // Add modal output hook (only once) if ( !self::$network_license_modal_added ) { add_action( 'admin_footer', [ __CLASS__, 'output_network_license_modal' ] ); self::$network_license_modal_added = true; } } elseif ( $this->isPro && $this->is_registered() ) { // Pro plugin is registered $new_links = [ 'license' => '' . __( 'Pro Version', $this->domain ) . '', ]; } else { // Free plugin $new_links = [ 'license' => sprintf( '' . __( 'Get the Pro Version', $this->domain ), $this->prefix ) . '', ]; } } else { // Regular admin - show settings and license status $new_links = [ 'settings' => sprintf( __( 'Settings', $this->domain ), $this->prefix ), 'license' => $this->is_registered() ? ( '' . __( 'Pro Version', $this->domain ) . '' ) : ( $isIssue ? ( sprintf( '' . __( 'License Issue', $this->domain ), $this->prefix ) . '' ) : ( sprintf( '' . __( 'Get the Pro Version', $this->domain ), $this->prefix ) . '' ) ), ]; } $links = array_merge( $new_links, $links ); } return $links; } /** * Output the network license registration modal. * Called via admin_footer hook in network admin. */ public static function output_network_license_modal() { $rest_url = esc_url( rest_url() ); $nonce = wp_create_nonce( 'wp_rest' ); ?>
prefix . '_reset_sub'] ) ) { delete_option( $this->prefix . '_pro_serial' ); delete_option( $this->prefix . '_license' ); return; } $html = 'It looks like you are using the free version of the plugin (%s) but a license for the Pro version was also found. The Pro version might have been replaced by the Free version during an update (might be caused by a temporarily issue). If it is the case, please download it again from the Meow Store. If you wish to continue using the free version and clear this message, click on this button.', $this->domain ), $this->nice_name_from_file( $this->mainfile ) ); $html .= '
'; $html .= '