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();
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' );
?>
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 .= '
';
wp_kses_post( $html );
}
public function admin_menu_start() {
// Hide the admin if user doesn't like Meow much
if ( get_option( 'meowapps_hide_meowapps', false ) ) {
register_setting( 'general', 'meowapps_hide_meowapps', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ] );
add_settings_field( 'meowapps_hide_ads', 'Meow Apps Menu', [ $this, 'meowapps_hide_dashboard_callback' ], 'general' );
return;
}
// Create standard menu if it does not already exist.
// The cat logo is injected as an inside the menu title (rather than passed as
// the $icon_url argument) so the original SVG fills are preserved — passing it via
// $icon_url makes WordPress add the .svg class and the admin color scheme strips
// the colors to a single fill.
global $submenu;
if ( !isset( $submenu[ 'meowapps-main-menu' ] ) ) {
add_menu_page(
'Meow Apps',
'Meow Apps',
'manage_options',
'meowapps-main-menu',
[ $this, 'admin_meow_apps' ],
'',
82
);
add_submenu_page(
'meowapps-main-menu',
__( 'Dashboard', $this->domain ),
__( 'Dashboard', $this->domain ),
'manage_options',
'meowapps-main-menu',
[ $this, 'admin_meow_apps' ]
);
}
// Position the cat icon so it sits in the standard icon column in both the
// expanded and the collapsed (folded) sidebar.
//
// The image lives inside the .wp-menu-name title (where the original code
// put it) so the renders with its native SVG fills. When the sidebar
// is collapsed, WP hides .wp-menu-name (and everything inside it), so a
// small JS snippet below clones the same into the .wp-menu-image
// slot — which WP keeps visible in both states. We use a real
// (not a background-image) on purpose: at small sizes WP's admin renders
// background-image SVGs noticeably lighter than the equivalent ,
// and we want the colored cat in both states.
//
// The !important rules also override an older "display: none" style that
// previous-generation Meow Apps common copies inject for .wp-menu-image;
// if any of them load first, ours still wins.
// Enqueue the menu-icon CSS/JS through the assets API instead of echoing inline tags.
add_action( 'admin_enqueue_scripts', function () {
$css = '
#toplevel_page_meowapps-main-menu .meowapps-menu-icon { width: 20px; height: auto; position: absolute; margin-left: -28px; margin-top: 3px; }
#toplevel_page_meowapps-main-menu .wp-menu-image { display: block !important; position: relative; }
#toplevel_page_meowapps-main-menu .wp-menu-image::before { display: none !important; }
#toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, calc(-50% - 3px)); width: 20px; height: auto; }
body.folded #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded { display: block; }
body.folded #toplevel_page_meowapps-main-menu .meowapps-menu-icon { display: none; }
@media only screen and (max-width: 960px) {
body.auto-fold #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded { display: block; }
body.auto-fold #toplevel_page_meowapps-main-menu .meowapps-menu-icon { display: none; }
}';
wp_add_inline_style( 'admin-menu', $css );
// Clone the in-title cat icon into the .wp-menu-image slot (CSS shows it only when folded).
$js = 'document.addEventListener("DOMContentLoaded", function () {'
. 'var li = document.getElementById("toplevel_page_meowapps-main-menu"); if ( !li ) { return; }'
. 'var src = li.querySelector(".meowapps-menu-icon"); var slot = li.querySelector(".wp-menu-image");'
. 'if ( !src || !slot || slot.querySelector(".meowapps-menu-icon-folded") ) { return; }'
. 'var clone = src.cloneNode(); clone.className = "meowapps-menu-icon-folded"; clone.removeAttribute("style"); slot.appendChild(clone);'
. '});';
wp_add_inline_script( 'common', $js );
} );
}
public function meowapps_hide_dashboard_callback() {
$html = '';
$html .= __( ' Hide Meow Apps menu and all its components, for a cleaner admin. This option will be reset if a new Meow Apps plugin is installed. Once activated, an option will be added in your General settings to display it again.', $this->domain );
echo MeowKit_MWCODE_Helpers::wp_kses( $html );
}
public function is_registered() {
$is_registered = apply_filters( $this->prefix . '_meowapps_is_registered', false, $this->prefix );
return $is_registered;
}
public function get_phpinfo() {
if ( !$this->is_user_admin() || !function_exists( 'phpinfo' ) ) {
return;
}
ob_start();
// phpcs:disable WordPress.PHP.DevelopmentFunctions
phpinfo( INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES );
// phpcs:enable
$html = ob_get_contents();
ob_end_clean();
$html = preg_replace( '%^.*(.*).*$%ms', '$1', $html );
return $html;
}
public function admin_meow_apps() {
$html = "";
$html .= "
";
$html .= $this->get_phpinfo();
$html .= '
';
$html = preg_replace( "/]+\>/i", '', $html );
echo wp_kses_post( $html );
}
public function admin_footer_text( $current ) {
return sprintf(
// translators: %1$s is the version of the interface; %2$s is a file path.
__( 'Thanks for using Meow Apps! This is the Meow Admin %1$s Loaded from %2$s ', $this->domain ),
MeowKit_MWCODE_Admin::$version,
__FILE__
);
}
/**
* Renders a promo banner on WordPress 7's Connectors page when AI Engine
* isn't installed. Kept self-contained so the common library stays simple:
* no new file, no REST endpoint, dismissal persists in localStorage.
*/
public function maybe_render_wpai_promo() {
// WordPress 7+ only.
if ( ! class_exists( 'WP_Connector_Registry' ) ) {
return;
}
// If AI Engine is installed, its own Connectors banner takes over.
if ( class_exists( 'Meow_MWAI_Core' ) ) {
return;
}
// Another Meow Apps plugin's common copy may have rendered already.
if ( defined( 'MEOWAPPS_WPAI_PROMO_RENDERED' ) ) {
return;
}
// Gate on the Connectors screen. The hook suffix differs between the
// direct file (`options-connectors.php`) and the menu-page variant.
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
$id = $screen ? $screen->id : ( isset( $GLOBALS['hook_suffix'] ) ? $GLOBALS['hook_suffix'] : '' );
$targets = array( 'options-connectors', 'options-connectors.php', 'settings_page_options-connectors-wp-admin' );
if ( ! in_array( $id, $targets, true ) ) {
return;
}
define( 'MEOWAPPS_WPAI_PROMO_RENDERED', true );
// Install button → WordPress's own plugin-install search page, pre-
// filtered for AI Engine. One click from there to install. This is the
// most reliable path: no custom nonces, native progress UI, native
// filesystem credential prompt if needed.
$can_install = current_user_can( 'install_plugins' );
$install_url = $can_install
? self_admin_url( 'plugin-install.php?tab=search&type=term&s=AI+Engine' )
: 'https://wordpress.org/plugins/ai-engine/';
$wporg_url = 'https://wordpress.org/plugins/ai-engine/';
$learn_url = 'https://meowapps.com/wordpress-7-ai-engine-gateway/';
// Title is split so "AI Engine" can carry an anchor to wp.org. Keeping
// the pieces as data (not one HTML string) avoids escaping surprises and
// keeps the translation unit stable.
$payload = array(
'titleBefore' => __( 'Highly recommended: Let ', 'meowapps' ),
'titleLink' => __( 'AI Engine', 'meowapps' ),
'titleAfter' => __( ' handle your connections.', 'meowapps' ),
'sub' => __( 'One plugin for every provider. Keep your AI setup clean and consistent: monitor your API costs in one place, log every single request, and avoid the mess of juggling separate plugins for each AI model.', 'meowapps' ),
'install' => __( 'Install AI Engine', 'meowapps' ),
'learn' => __( 'Learn more', 'meowapps' ),
'dismiss' => __( 'Dismiss', 'meowapps' ),
'installUrl' => $install_url,
'wporgUrl' => $wporg_url,
'learnUrl' => $learn_url,
);
wp_register_style( 'meowapps-common-inline', false );
wp_enqueue_style( 'meowapps-common-inline' );
ob_start();
?>
.meowapps-wpai-promo {
margin: 0 0 16px; padding: 14px 18px;
display: flex; align-items: flex-start; gap: 14px;
background: #f0f4ff; border: 1px solid #d6deff; border-radius: 4px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 13px; line-height: 1.45; color: #1e1e1e;
}
.meowapps-wpai-promo-icon {
width: 32px; height: 32px; border-radius: 50%;
background: #2f5fff; color: #fff;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
margin-top: 1px;
}
.meowapps-wpai-promo-icon svg { width: 18px; height: 18px; display: block; }
.meowapps-wpai-promo-body {
flex: 1; min-width: 0;
display: flex; flex-direction: column; gap: 10px;
}
.meowapps-wpai-promo-text strong { font-weight: 700; display: block; margin-bottom: 3px; }
.meowapps-wpai-promo-text span { color: #50575e; }
.meowapps-wpai-promo-titlelink {
color: #2f5fff; text-decoration: none; border-bottom: 1px dashed #2f5fff;
}
.meowapps-wpai-promo-titlelink:hover { color: #2448cc; border-bottom-color: #2448cc; }
.meowapps-wpai-promo-actions {
display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
}
.meowapps-wpai-promo-btn {
appearance: none; border: 1px solid transparent; border-radius: 4px;
padding: 7px 16px; font: inherit; font-size: 12.5px; font-weight: 600;
cursor: pointer; text-decoration: none;
transition: background 0.12s ease, border-color 0.12s ease;
}
.meowapps-wpai-promo-btn-primary { background: #2f5fff; color: #fff; }
.meowapps-wpai-promo-btn-primary:hover { background: #2448cc; color: #fff; }
.meowapps-wpai-promo-btn-secondary { background: #7c3aed; color: #fff; }
.meowapps-wpai-promo-btn-secondary:hover { background: #6527c9; color: #fff; }
.meowapps-wpai-promo-btn-dismiss {
margin-left: auto;
background: transparent; color: #6b7280;
font-weight: 500; padding: 7px 10px;
}
.meowapps-wpai-promo-btn-dismiss:hover { color: #1e1e1e; background: rgba(0,0,0,0.04); }
(function () {
var D = ;
try { if (localStorage.getItem('meowapps-wpai-promo-dismissed') === '1') return; } catch (e) {}
function build() {
var host = document.createElement('div');
host.className = 'meowapps-wpai-promo';
host.setAttribute('role', 'status');
var iconSvg = '';
host.innerHTML = [
'', iconSvg, '',
'
',
'
',
' ',
'
',
'',
'
'
].join('');
// Title carries an anchor on "AI Engine" → wp.org plugin page.
var strong = host.querySelector('strong');
strong.appendChild(document.createTextNode(D.titleBefore));
var tLink = document.createElement('a');
tLink.href = D.wporgUrl;
tLink.target = '_blank';
tLink.rel = 'noopener noreferrer';
tLink.className = 'meowapps-wpai-promo-titlelink';
tLink.textContent = D.titleLink;
strong.appendChild(tLink);
strong.appendChild(document.createTextNode(D.titleAfter));
host.querySelector('.meowapps-wpai-promo-sub').textContent = D.sub;
var actions = host.querySelector('.meowapps-wpai-promo-actions');
function link(label, cls, href, newTab) {
var a = document.createElement('a');
a.className = 'meowapps-wpai-promo-btn ' + cls;
a.textContent = label;
a.href = href;
if (newTab) { a.target = '_blank'; a.rel = 'noopener noreferrer'; }
actions.appendChild(a);
return a;
}
// Install → WordPress's plugin-install search page, pre-filtered.
// User lands on a familiar screen with AI Engine as the top hit and
// can install with the native WordPress UX.
link(D.install, 'meowapps-wpai-promo-btn-primary', D.installUrl, false);
link(D.learn, 'meowapps-wpai-promo-btn-secondary', D.learnUrl, true);
var d = document.createElement('button');
d.type = 'button';
d.className = 'meowapps-wpai-promo-btn meowapps-wpai-promo-btn-dismiss';
d.textContent = D.dismiss;
d.addEventListener('click', function () {
try { localStorage.setItem('meowapps-wpai-promo-dismissed', '1'); } catch (e) {}
if (host.parentNode) host.parentNode.removeChild(host);
});
actions.appendChild(d);
return host;
}
function ensure() {
if (document.querySelector('.meowapps-wpai-promo')) return;
var page = document.querySelector('.connectors-page');
if (page) { page.insertBefore(build(), page.firstChild); return; }
var header = document.querySelector('.boot-layout__stage header');
if (header && header.parentNode) {
header.parentNode.insertBefore(build(), header.nextSibling);
}
}
var tries = 0;
var iv = setInterval(function () {
ensure();
if (document.querySelector('.meowapps-wpai-promo') || ++tries > 40) clearInterval(iv);
}, 120);
var app = document.getElementById('options-connectors-wp-admin-app')
|| document.getElementById('options-connectors-app');
if (app && 'MutationObserver' in window) {
new MutationObserver(ensure).observe(app, { childList: true, subtree: true });
}
})();