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_enqueue_scripts', [ $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(); // The licenser already knows WHY validation failed and stores it as // license['issue'] (licenser.php), but this badge used to render a flat // "License Issue" for every case. That sent people to support to ask a // question the plugin could have answered: the most common one by far is a // licence whose activation slot is held by another site, which reads as // "my licence is broken" and gets reported as a billing problem. $issueLabel = __( 'License Issue', $this->domain ); if ( $isIssue ) { $license = get_option( $this->prefix . '_license', '' ); $issue = is_array( $license ) && !empty( $license['issue'] ) ? $license['issue'] : null; // Codes the store actually sends. Verified against EDD Software Licensing // rather than guessed: an invented key would silently never match, and a // wrong label is worse than the generic one. $labels = [ // `error` codes, from a failed activation. 'no_activations_left' => __( 'License in use on another site', $this->domain ), 'expired' => __( 'License expired', $this->domain ), 'disabled' => __( 'License revoked', $this->domain ), 'missing' => __( 'License key not recognized', $this->domain ), 'key_mismatch' => __( 'License key not recognized', $this->domain ), 'item_name_mismatch' => __( 'License is for another plugin', $this->domain ), 'invalid_item_id' => __( 'License is for another plugin', $this->domain ), 'missing_item_id' => __( 'License is for another plugin', $this->domain ), 'bundle_activation_not_allowed' => __( 'This license cannot be activated directly', $this->domain ), // `license` statuses, when the key is known but not valid here. 'site_inactive' => __( 'License not activated on this site', $this->domain ), 'inactive' => __( 'License not activated on this site', $this->domain ), // Genuinely no answer from the store. NOT invalid_response, which means the // store replied and we could not make sense of it: blaming the connection // there would send people chasing a firewall that is working fine. 'no_response' => __( 'License server unreachable', $this->domain ), ]; if ( $issue !== null && isset( $labels[ $issue ] ) ) { $issueLabel = $labels[ $issue ]; } } 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 ? ( '' . esc_html( $issueLabel ) . '' ) : ( 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' ); ?>
(function() { var modal = document.getElementById('meowapps-network-license-modal'); var input = document.getElementById('meowapps-license-key-input'); var message = document.getElementById('meowapps-license-message'); var pluginName = document.getElementById('meowapps-license-plugin-name'); var submitBtn = document.getElementById('meowapps-license-submit'); var cancelBtn = document.getElementById('meowapps-license-cancel'); var currentPrefix = ''; function showMessage(text, isError) { message.textContent = text; message.style.display = 'block'; message.style.background = isError ? '#fcf0f1' : '#edfaef'; message.style.color = isError ? '#d63638' : '#1e7e34'; message.style.border = '1px solid ' + (isError ? '#d63638' : '#1e7e34'); } function hideMessage() { message.style.display = 'none'; } function openModal(prefix, plugin) { currentPrefix = prefix; pluginName.textContent = plugin; input.value = ''; hideMessage(); submitBtn.disabled = false; submitBtn.textContent = 'Validate & Register'; modal.style.display = 'flex'; input.focus(); } function closeModal() { modal.style.display = 'none'; currentPrefix = ''; } // Handle click on "Register License" links document.addEventListener('click', function(e) { if (e.target.classList.contains('meowapps-network-license-link')) { e.preventDefault(); var prefix = e.target.getAttribute('data-prefix'); var plugin = e.target.getAttribute('data-plugin'); openModal(prefix, plugin); } }); // Close modal on cancel or clicking outside cancelBtn.addEventListener('click', closeModal); modal.addEventListener('click', function(e) { if (e.target === modal) closeModal(); }); // Handle escape key document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && modal.style.display === 'flex') { closeModal(); } }); // Handle enter key in input input.addEventListener('keydown', function(e) { if (e.key === 'Enter') { submitBtn.click(); } }); // Submit license submitBtn.addEventListener('click', function() { var licenseKey = input.value.trim(); if (!licenseKey) { showMessage('Please enter a license key.', true); return; } submitBtn.disabled = true; submitBtn.textContent = 'Validating...'; hideMessage(); var restUrl = 'meow-licenser/' + currentPrefix + '/v1/set_license/'; fetch(restUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': '' }, body: JSON.stringify({ serialKey: licenseKey }) }) .then(function(response) { return response.json(); }) .then(function(data) { if (data.success && data.data && !data.data.issue) { showMessage('License registered successfully! Reloading...', false); setTimeout(function() { location.reload(); }, 1500); } else { var errorMsg = 'License validation failed.'; if (data.data && data.data.issue) { errorMsg = 'License issue: ' + data.data.issue; } showMessage(errorMsg, true); submitBtn.disabled = false; submitBtn.textContent = 'Validate & Register'; } }) .catch(function(error) { showMessage('Error: ' + error.message, true); submitBtn.disabled = false; submitBtn.textContent = 'Validate & Register'; }); }); })(); prefix . '_reset_sub'] ) && isset( $_POST[ $this->prefix . '_reset_sub_nonce' ] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ $this->prefix . '_reset_sub_nonce' ] ) ), $this->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 .= '