PluginProbe
Code Engine – PHP Snippets, AI Functions & Automation for WordPress / 0.5.7
Code Engine – PHP Snippets, AI Functions & Automation for WordPress v0.5.7
0.5.7 0.5.6 0.5.5 0.5.4 0.5.3 0.5.2 0.5.1 0.5.0 0.4.9 0.4.8 0.4.7 0.4.6 trunk 0.0.1 0.0.2 0.2.8 0.2.9 0.3.0 0.3.1 0.3.2 0.3.3 0.3.4 0.3.5 0.3.6 0.3.7 All 33 releases
← All changes | common/admin.php +626 -57 0.3.70.5.7 View file →
@@ -1,69 +1,194 @@
1 1 <?php
2 2
3 -if ( !class_exists( 'MeowCommon_Admin' ) ) {
3 +if ( !class_exists( 'MeowKit_MWCODE_Admin' ) ) {
4 4
5 - class MeowCommon_Admin {
5 + class MeowKit_MWCODE_Admin {
6 6 public static $loaded = false;
7 - public static $version = '4.0';
8 - public static $admin_version = '4.0';
7 + public static $version = '5.0';
8 + public static $admin_version = '5.0';
9 + public static $network_license_modal_added = false;
10 + public static $network_license_plugins = [];
9 11
12 + /**
13 + * Storage for instances that need deferred initialization.
14 + *
15 + * WordPress Loading Sequence Problem:
16 + * 1. Load all plugin files
17 + * 2. Fire 'plugins_loaded' hook ← Most plugins instantiate Admin here
18 + * 3. Load wp-includes/pluggable.php ← current_user_can() defined here
19 + * 4. Fire 'init' hook ← Safe to use pluggable functions
20 + *
21 + * When plugins instantiate during 'plugins_loaded', the pluggable functions
22 + * (current_user_can, wp_get_current_user) don't exist yet. This array stores
23 + * instances until 'init' when we can safely call those functions.
24 + *
25 + * @var array
26 + */
27 + private static $deferred_instances = array();
28 +
10 29 public $prefix; // prefix used for actions, filters (mfrh)
11 30 public $mainfile; // plugin main file (media-file-renamer.php)
12 31 public $domain; // domain used for translation (media-file-renamer)
13 32 public $isPro = false;
14 33
15 - public static $logo = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAxNDM0IDk0NyI+CiAgPHBhdGggZmlsbD0iIzAwMCIgZD0iTTgwNSA3NzdhNzkyIDc5MiAwIDAgMS0yNjItNDMgODExIDgxMSAwIDAgMS0yODYtMTY0QTk1OSA5NTkgMCAwIDEgNiAyMDAgMTU4IDE1OCAwIDAgMSAzMDQgOTdjNDEgOTYgOTQgMTc1IDE1OSAyMzNhNDk3IDQ5NyAwIDAgMCAzNzYgMTI5IDYwIDYwIDAgMCAxIDY3IDYwbDI3IDE4NmM0IDMzLTE4IDYzLTUxIDY4LTYgMC0zNCA0LTc3IDRaTTEyMiAxNjhsMiA1YTg0MSA4NDEgMCAwIDAgMjEyIDMwNyA2OTIgNjkyIDAgMCAwIDQ2OSAxNzdsLTExLTc2YTYxNiA2MTYgMCAwIDEtNDEyLTE2MiA3NjkgNzY5IDAgMCAxLTE4OC0yNzYgMzggMzggMCAwIDAtNTAtMjBjLTE4IDctMjcgMjctMjIgNDVaIi8+CiAgPHBhdGggZmlsbD0iI0ZEQTk2MCIgZD0ibTY0IDE4NCA0IDEyYTkwMCA5MDAgMCAwIDAgMjI4IDMyOSA3NTIgNzUyIDAgMCAwIDU3NyAxODhsLTI3LTE5NGE1NjMgNTYzIDAgMCAxLTQyMy0xNDQgNzA5IDcwOSAwIDAgMS0xNzQtMjU1IDk4IDk4IDAgMCAwLTE4NSA2NFoiLz4KICA8bWFzayBpZD0iYSIgd2lkdGg9IjgxNCIgaGVpZ2h0PSI2NTciIHg9IjYwIiB5PSI2MCIgbWFza1VuaXRzPSJ1c2VyU3BhY2VPblVzZSIgc3R5bGU9Im1hc2stdHlwZTpsdW1pbmFuY2UiPgogICAgPHBhdGggZmlsbD0iI2ZmZiIgZD0ibTY0IDE4NCA0IDEyYTkwMCA5MDAgMCAwIDAgMjI4IDMyOSA3NTIgNzUyIDAgMCAwIDU3NyAxODhsLTI3LTE5NGE1NjMgNTYzIDAgMCAxLTQyMy0xNDQgNzA5IDcwOSAwIDAgMS0xNzQtMjU1IDk4IDk4IDAgMCAwLTE4NSA2NFoiLz4KICA8L21hc2s+CiAgPGcgbWFzaz0idXJsKCNhKSI+CiAgICA8cGF0aCBmaWxsPSIjODA0NjI1IiBkPSJNMTIwIDUzMmMtNDEgMC04NC01LTEzMC0xNWwzMS0xNDVjMTAxIDIxIDE4MCAxMiAyMzMtMjcgNzAtNTEgODAtMTQxIDgwLTE0MmwxNDkgMTNhMzYzIDM2MyAwIDAgMS0xMzkgMjQ4IDM1MSAzNTEgMCAwIDEtMjI0IDY4Wm0zNjkgMTc1YzQ3LTMxIDg0LTcxIDExMC0xMTYgMzItNTYgNDYtMTIzIDQyLTE5Mi0zLTUxLTE1LTg3LTE2LTkxbC0xNDEgNDhhMjI1IDIyNSAwIDAgMS0xNSAxNjFjLTMzIDU4LTEwMSA5OS0yMDMgMTIwbDMwIDE0NmM3Ni0xNiAxNDEtNDEgMTkzLTc2Wk02MiAyNjljNjQtNCAxMjItMjIgMTc0LTUzQTQxMyA0MTMgMCAwIDAgNDIxLTQ3TDE4NC05MnYtMXMtMTYgNzEtNzMgMTAzQzkyIDIxIDcwIDI3IDQ0IDI5IDcgMzEtMzcgMjQtODYgOGwtNzQgMjI5YTYyMyA2MjMgMCAwIDAgMjIyIDMyWiIvPgogIDwvZz4KICA8cGF0aCBmaWxsPSIjMDAwIiBkPSJNMTM3MyA5NDdoLTExMGMtMzMgMC02MC0yNy02MC02MHYtOTdsLTM2IDg3YTYyIDYyIDAgMCAxLTU2IDM3aC03OWMtMjUgMC00Ni0xNC01Ni0zN2wtMzYtODd2OTdjMCAzMy0yNyA2MC02MCA2MEg3NjljLTMzIDAtNjAtMjctNjAtNjBWMzE2YzAtMzMgMjctNjAgNjAtNjBoMTQxYzI0IDAgNDYgMTUgNTUgMzdsMTA2IDI1OCAxMDctMjU4YzktMjIgMzEtMzcgNTUtMzdoMTQwYzM0IDAgNjAgMjcgNjAgNjB2NTcxYzAgMzMtMjYgNjAtNjAgNjBabS0zMTYtMTg4IDE0IDM0IDE1LTM0LTExIDFoLTdsLTExLTFabTE5OS0zMTRoN2MyMSAwIDQwIDExIDUwIDI4di05N2gtNDBsLTI5IDcwIDEyLTFabS00MjctNjl2OTdjMTEtMTcgMjktMjggNTEtMjhoNmwxMyAxLTI5LTcwaC00MVoiLz4KICA8cGF0aCBmaWxsPSIjZmZmIiBkPSJNNzY5IDg4N1YzMTZoMTQxbDE1OCAzODRoN2wxNTgtMzg0aDE0MHY1NzFoLTExMFY1MDVoLTdsLTE0NSAzNDloLTc5TDg4NiA1MDVoLTZ2MzgySDc2OVoiLz4KPC9zdmc+Cg==';
34 + // Store constructor params that affect per-instance setup
35 + private $disableReview = false;
16 36
37 + public static $logo = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNDYiIHZpZXdCb3g9IjAgMCA2NCA0NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGlwUGF0aD0idXJsKCNjbGlwMF8zMTBfMjI5KSI+CiAgICA8cGF0aCBkPSJNNjQgMzAuNjQwOEM2NCAyNy43OTg1IDYwLjA4MTYgMjUuODMwMyA1NS44Mjk4IDI1LjgzMDNDNTQuODU5MyAyNS44MzAzIDUzLjkzMTEgMjUuOTMzIDUzLjA3NiAyNi4xMjUzQzQ5Ljg4NjUgMTkuMDc5IDQxLjY1MzkgMTMuMDg1MyAzMi4wMDAyIDEzLjA4NTNDMzAuODMzNyAxMy4wODUzIDI5LjY4ODEgMTMuMTcyNyAyOC41Njk4IDEzLjMzOTJDMjcuMjA2OSAxMC4zMDc2IDIyLjY3NjIgMi40MzQyNiAxMS41OTU0IDAuMDgzMDA2NEMxMS4wNDkxIC0wLjAzMjc0NTYgMTAuNDk0NiAwLjI0MDU3OCAxMC4yNTkgMC43NDY5QzguODU5MTMgMy43NTYwOCA0Ljc0MjQ3IDE0LjQxMTYgMTAuMjQwMyAyNS45OTMxQzkuNTgxNjUgMjUuODg2NCA4Ljg4NzUxIDI1LjgzMDMgOC4xNzAyMiAyNS44MzAzQzMuOTE4MzkgMjUuODMwMyAwIDI3Ljc5ODUgMCAzMC42NDA4QzAgMzMuNDgzIDMuOTE4MzkgMzUuMjI3MiA4LjE3MDIyIDM1LjIyNzJDOC43MTEyNyAzNS4yMjcyIDkuMjM5MjUgMzUuMTk4OCA5Ljc0ODkzIDM1LjE0MzVDOS40MzYwMiAzNS4yNjY0IDkuMTIyNzUgMzUuNDA3NSA4LjgxMTcxIDM1LjU2NzdDNS42OTM4OCAzNy4xNzA3IDMuOTY4OCA0MC4wMzEyIDQuOTU5MDQgNDEuOTU2OEM1Ljk0ODkgNDMuODgyNCA5LjI3OTIgNDQuMTQ0MiAxMi4zOTcgNDIuNTQxMkMxMy4wNDY0IDQyLjIwNzQgMTMuNjM0OCA0MS44MTkgMTQuMTUxNiA0MS4zOTZDMTguMjYyNyA0NC40OTY3IDI0LjcyODMgNDUuOTgwOSAzMS45OTk4IDQ1Ljk4MDlDMzkuMjcxMyA0NS45ODA5IDQ1LjczNyA0NC40OTY3IDQ5Ljg0OCA0MS4zOTZDNTAuMzY0NCA0MS44MTkgNTAuOTUzMyA0Mi4yMDc0IDUxLjYwMjYgNDIuNTQxMkM1NC43MjA0IDQ0LjE0NDIgNTguMDUwMyA0My44ODI0IDU5LjA0MDYgNDEuOTU2OEM2MC4wMzA1IDQwLjAzMTIgNTguMzA1NyAzNy4xNzA3IDU1LjE4NzkgMzUuNTY3N0M1NC44NzYxIDM1LjQwNzUgNTQuNTYyMSAzNS4yNjY3IDU0LjI0ODUgMzUuMTQzNUM1NC43NTg5IDM1LjE5ODggNTUuMjg3NiAzNS4yMjc1IDU1LjgyOTQgMzUuMjI3NUM2MC4wODEyIDM1LjIyNzUgNjMuOTk5NiAzMy40ODM0IDYzLjk5OTYgMzAuNjQxMUw2NCAzMC42NDA4WiIgZmlsbD0id2hpdGUiLz4KICAgIDxwYXRoIGQ9Ik0yMi4yMjkzIDM2Ljc0NDNDMjYuNTkzNSAzNi43NDQzIDMwLjEzMTQgMzMuMjA2NCAzMC4xMzE0IDI4Ljg0MjJDMzAuMTMxNCAyNC40NzggMjYuNTkzNSAyMC45NDAxIDIyLjIyOTMgMjAuOTQwMUMxNy44NjUxIDIwLjk0MDEgMTQuMzI3MSAyNC40NzggMTQuMzI3MSAyOC44NDIyQzE0LjMyNzEgMzMuMjA2NCAxNy44NjUxIDM2Ljc0NDMgMjIuMjI5MyAzNi43NDQzWiIgZmlsbD0iIzAwRTI4RSIvPgogICAgPHBhdGggZD0iTTIyLjI2NTUgMzMuMTM2MUMyMy41MDIyIDMzLjEzNjEgMjQuNTA0NyAzMS4yODA1IDI0LjUwNDcgMjguOTkxNUMyNC41MDQ3IDI2LjcwMjQgMjMuNTAyMiAyNC44NDY4IDIyLjI2NTUgMjQuODQ2OEMyMS4wMjg4IDI0Ljg0NjggMjAuMDI2MiAyNi43MDI0IDIwLjAyNjIgMjguOTkxNUMyMC4wMjYyIDMxLjI4MDUgMjEuMDI4OCAzMy4xMzYxIDIyLjI2NTUgMzMuMTM2MVoiIGZpbGw9IiMzQzZFOEIiLz4KICAgIDxwYXRoIGQ9Ik0zMS45OTk4IDM3LjkxNTZDMzMuNDIzNyAzNy45MTU2IDM0LjU3ODEgMzcuMzQwOSAzNC41NzgxIDM2LjYzMTlDMzQuNTc4MSAzNS45MjI5IDMzLjQyMzcgMzUuMzQ4MSAzMS45OTk4IDM1LjM0ODFDMzAuNTc1OCAzNS4zNDgxIDI5LjQyMTUgMzUuOTIyOSAyOS40MjE1IDM2LjYzMTlDMjkuNDIxNSAzNy4zNDA5IDMwLjU3NTggMzcuOTE1NiAzMS45OTk4IDM3LjkxNTZaIiBmaWxsPSIjRkY5NDkzIi8+CiAgICA8cGF0aCBkPSJNNTQuMjUwMyAzNS4xMDU4QzU0Ljc2IDM1LjE2MTEgNTUuMjg3OSAzNS4xODk0IDU1LjgyOSAzNS4xODk0QzYwLjA4MDggMzUuMTg5NCA2My45OTkyIDMzLjQ0NTMgNjMuOTk5MiAzMC42MDNDNjMuOTk5MiAyNy43NjA4IDYwLjA4MDggMjUuNzkyNiA1NS44MjkgMjUuNzkyNkM1NS4xMTE3IDI1Ljc5MjYgNTQuNDE3NiAyNS44NDkgNTMuNzU4NSAyNS45NTU4QzU5LjI1NjcgMTQuMzc0MiA1NS4xMzk3IDMuNzE4NzIgNTMuNzQwMiAwLjcwOTU0NkM1My41MDQ2IDAuMjAzMjI1IDUyLjk1MDEgLTAuMDcwMDk5MSA1Mi40MDM4IDAuMDQ1NjUyOUM0MS4zMjMgMi4zOTY5MSAzNi43OTIzIDEwLjI3MDcgMzUuNDI5OCAxMy4zMDE1QzM0LjQ1NDEgMTMuMTU2NiAzMy40NTc5IDEzLjA3MTEgMzIuNDQ1MiAxMy4wNTE3QzMxLjI3NDMgMjAuMDMzIDI4Ljk2NTYgNDMuOTM2NSA1NC4zNDM2IDM1LjE0MzlDNTQuMzEyMyAzNS4xMzEyIDU0LjI4MTMgMzUuMTE4MSA1NC4yNDk5IDM1LjEwNThINTQuMjUwM1oiIGZpbGw9IiMyQjlERkYiLz4KICAgIDxwYXRoIGQ9Ik00MS43MzQyIDMzLjEzNjFDNDIuOTcwOSAzMy4xMzYxIDQzLjk3MzUgMzEuMjgwNSA0My45NzM1IDI4Ljk5MTVDNDMuOTczNSAyNi43MDI0IDQyLjk3MDkgMjQuODQ2OCA0MS43MzQyIDI0Ljg0NjhDNDAuNDk3NSAyNC44NDY4IDM5LjQ5NSAyNi43MDI0IDM5LjQ5NSAyOC45OTE1QzM5LjQ5NSAzMS4yODA1IDQwLjQ5NzUgMzMuMTM2MSA0MS43MzQyIDMzLjEzNjFaIiBmaWxsPSIjM0M2RThCIi8+CiAgPC9nPgogIDxkZWZzPgogICAgPGNsaXBQYXRoIGlkPSJjbGlwMF8zMTBfMjI5Ij4KICAgICAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjQ1Ljk2MTciIGZpbGw9IndoaXRlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDAuMDE5MTY1KSIvPgogICAgPC9jbGlwUGF0aD4KICA8L2RlZnM+Cjwvc3ZnPgo=';
38 +
17 39 public function __construct( $prefix, $mainfile, $domain, $isPro = false, $disableReview = false, $freeOnly = false ) {
18 40
19 - if ( !MeowCommon_Admin::$loaded ) {
20 - if ( is_admin() ) {
21 -
22 - if ( MeowCommon_Helpers::is_asynchronous_request() ) {
23 - return;
24 - }
25 -
26 - // Check potential issues with this WordPress install, other plugins, etc.
27 - new MeowCommon_Issues( $prefix, $mainfile, $domain );
28 -
29 - // Create the Meow Apps Menu (priority 5 to ensure it's created early)
30 - add_action( 'admin_menu', [ $this, 'admin_menu_start' ], 5 );
31 - $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : null;
32 - if ( $page === 'meowapps-main-menu' ) {
33 - add_filter( 'admin_footer_text', [ $this, 'admin_footer_text' ], 100000, 1 );
34 - }
35 - }
36 - MeowCommon_Admin::$loaded = true;
37 - }
38 -
39 - // Variables for this plugin
41 + // ALWAYS set instance properties first - these are needed regardless of when setup runs
40 42 $this->prefix = $prefix;
41 43 $this->mainfile = $mainfile;
42 44 $this->domain = $domain;
43 45 $this->isPro = $isPro;
46 + $this->disableReview = $disableReview;
44 47
45 - // If there is no mainfile, it's either a Pro only Plugin (with no Free version available) or a Theme.
48 + if ( is_admin() ) {
46 49
47 - if ( is_admin() ) {
50 + // Skip AJAX and REST requests to avoid unnecessary processing
51 + if ( MeowKit_MWCODE_Helpers::is_asynchronous_request() ) {
52 + return;
53 + }
54 +
55 + // Check if WordPress pluggable functions are available yet.
56 + // These are defined in wp-includes/pluggable.php, which WordPress loads
57 + // AFTER the 'plugins_loaded' hook but BEFORE the 'init' hook.
58 + if ( !function_exists( 'current_user_can' ) || !function_exists( 'wp_get_current_user' ) ) {
59 + // Functions don't exist yet - defer admin setup until 'init' hook
60 + // This is NORMAL behavior when plugins instantiate on 'plugins_loaded'
61 + $this->defer_admin_setup();
62 + // Continue to rest of constructor (filters, license checks, etc.)
63 + } else {
64 + // Functions already exist - safe to run admin setup immediately
65 + // This happens when plugins instantiate on 'init' or later
66 + $this->run_admin_setup();
67 + }
68 +
69 + // License-related admin notices (doesn't require pluggable functions)
48 70 $license = get_option( $this->prefix . '_license', '' );
49 71 if ( !empty( $license ) && !$this->isPro ) {
50 72 add_action( 'admin_notices', [ $this, 'admin_notices_licensed_free' ] );
51 73 }
52 - if ( $this->is_user_admin() ) {
53 - if ( !$disableReview ) {
54 - new MeowCommon_Ratings( $prefix, $mainfile, $domain );
55 - }
56 - new MeowCommon_News( $domain );
57 - }
58 74 }
75 +
76 + // ALWAYS register these filters (they work at any time)
59 77 add_filter( 'plugin_row_meta', [ $this, 'custom_plugin_row_meta' ], 10, 2 );
60 78 add_filter( 'edd_sl_api_request_verify_ssl', [ $this, 'request_verify_ssl' ], 10, 0 );
61 79 }
62 80
81 + /**
82 + * Defer admin setup until WordPress 'init' hook.
83 + *
84 + * This method stores the current instance and registers a one-time
85 + * 'init' hook callback that will process all deferred instances.
86 + *
87 + * Why defer? Because we need current_user_can() to check permissions,
88 + * and that function doesn't exist until after 'plugins_loaded'.
89 + */
90 + private function defer_admin_setup() {
91 + // Add this instance to the queue for processing on 'init'
92 + self::$deferred_instances[] = $this;
93 +
94 + // Register the 'init' hook only once (for the first deferred instance)
95 + if ( count( self::$deferred_instances ) === 1 ) {
96 + add_action( 'init', array( __CLASS__, 'process_deferred_instances' ) );
97 + }
98 + }
99 +
100 + /**
101 + * Static callback for 'init' hook - processes all deferred instances.
102 + *
103 + * By the time 'init' fires, WordPress has loaded pluggable.php and
104 + * current_user_can() is guaranteed to exist. We process all instances
105 + * that were created during 'plugins_loaded' or earlier.
106 + *
107 + * This is called as a static method because it processes multiple instances.
108 + */
109 + public static function process_deferred_instances() {
110 + // Belt-and-suspenders check: pluggable functions should ALWAYS exist by 'init'
111 + // If they somehow don't, log a warning and bail (this should never happen)
112 + if ( !function_exists( 'current_user_can' ) || !function_exists( 'wp_get_current_user' ) ) {
113 + trigger_error(
114 + 'MeowKit_MWCODE_Admin: Pluggable functions still unavailable on init hook. ' .
115 + 'This should never happen and indicates a serious WordPress core issue.',
116 + E_USER_WARNING
117 + );
118 + return;
119 + }
120 +
121 + // Process each deferred instance's admin setup
122 + foreach ( self::$deferred_instances as $instance ) {
123 + $instance->run_admin_setup();
124 + }
125 +
126 + // Clear the array to free memory (we won't need these references anymore)
127 + self::$deferred_instances = array();
128 + }
129 +
130 + /**
131 + * Run admin setup - both shared (once) and per-instance (each plugin).
132 + *
133 + * SHARED SETUP (once for all plugins):
134 + * - Issues detection
135 + * - Meow Apps menu creation
136 + * - Admin footer customization
137 + *
138 + * PER-INSTANCE SETUP (once per plugin):
139 + * - Ratings system
140 + * - News system
141 + *
142 + * This method is called either immediately (if pluggable functions exist)
143 + * or deferred until 'init' (if they don't). Either way, it's safe to call
144 + * current_user_can() here.
145 + */
146 + private function run_admin_setup() {
147 + // SHARED SETUP: Only run once for all Meow Apps plugins
148 + if ( !MeowKit_MWCODE_Admin::$loaded ) {
149 + // Check for potential issues with WordPress install, other plugins, etc.
150 + new MeowKit_MWCODE_Issues( $this->prefix, $this->mainfile, $this->domain );
151 +
152 + // Create the unified Meow Apps menu (priority 5 to ensure early creation)
153 + add_action( 'admin_menu', [ $this, 'admin_menu_start' ], 5 );
154 +
155 + // Customize admin footer on Meow Apps pages
156 + $page = isset( $_GET['page'] ) ? sanitize_text_field( $_GET['page'] ) : null;
157 + if ( $page === 'meowapps-main-menu' ) {
158 + add_filter( 'admin_footer_text', [ $this, 'admin_footer_text' ], 100000, 1 );
159 + }
160 +
161 + // Promote AI Engine on the WordPress 7 Connectors page when AI Engine
162 + // itself isn't installed. When AI Engine is active, its own banner
163 + // takes over — so this path only runs on "bare" Meow Apps installs.
164 + add_action( 'admin_enqueue_scripts', [ $this, 'maybe_render_wpai_promo' ] );
165 +
166 + MeowKit_MWCODE_Admin::$loaded = true;
167 + }
168 +
169 + // PER-INSTANCE SETUP: Run for each plugin that uses this library
170 + // Only admins get ratings prompts and news
171 + if ( $this->is_user_admin() ) {
172 + if ( !$this->disableReview ) {
173 + new MeowKit_MWCODE_Ratings( $this->prefix, $this->mainfile, $this->domain );
174 + }
175 + new MeowKit_MWCODE_News( $this->domain );
176 + }
177 + }
178 +
179 + /**
180 + * Check if current user is a site administrator.
181 + *
182 + * This method is only called from run_admin_setup(), which guarantees
183 + * that pluggable functions exist. No error logging needed - if the
184 + * functions don't exist, we simply return false as a defensive fallback.
185 + *
186 + * @return bool True if user can manage options, false otherwise
187 + */
63 188 public function is_user_admin() {
189 + // Defensive check (should never fail if called from run_admin_setup)
64 190 if ( !function_exists( 'current_user_can' ) || !function_exists( 'wp_get_current_user' ) ) {
65 - error_log( 'MeowCommon_Admin is called too early. Please make sure it is called after the plugins_loaded filter.' );
66 191 return false;
67 192 }
68 193 return current_user_can( 'manage_options' );
69 194 }
@@ -77,22 +202,233 @@
77 202 if ( !$isActive ) {
78 203 return $links;
79 204 }
80 205 $isIssue = $this->isPro && !$this->is_registered();
206 +
207 + // The licenser already knows WHY validation failed and stores it as
208 + // license['issue'] (licenser.php), but this badge used to render a flat
209 + // "License Issue" for every case. That sent people to support to ask a
210 + // question the plugin could have answered: the most common one by far is a
211 + // licence whose activation slot is held by another site, which reads as
212 + // "my licence is broken" and gets reported as a billing problem.
213 + $issueLabel = __( 'License Issue', $this->domain );
214 + if ( $isIssue ) {
215 + $license = get_option( $this->prefix . '_license', '' );
216 + $issue = is_array( $license ) && !empty( $license['issue'] ) ? $license['issue'] : null;
217 + // Codes the store actually sends. Verified against EDD Software Licensing
218 + // rather than guessed: an invented key would silently never match, and a
219 + // wrong label is worse than the generic one.
220 + $labels = [
221 + // `error` codes, from a failed activation.
222 + 'no_activations_left' => __( 'License in use on another site', $this->domain ),
223 + 'expired' => __( 'License expired', $this->domain ),
224 + 'disabled' => __( 'License revoked', $this->domain ),
225 + 'missing' => __( 'License key not recognized', $this->domain ),
226 + 'key_mismatch' => __( 'License key not recognized', $this->domain ),
227 + 'item_name_mismatch' => __( 'License is for another plugin', $this->domain ),
228 + 'invalid_item_id' => __( 'License is for another plugin', $this->domain ),
229 + 'missing_item_id' => __( 'License is for another plugin', $this->domain ),
230 + 'bundle_activation_not_allowed' => __( 'This license cannot be activated directly', $this->domain ),
231 + // `license` statuses, when the key is known but not valid here.
232 + 'site_inactive' => __( 'License not activated on this site', $this->domain ),
233 + 'inactive' => __( 'License not activated on this site', $this->domain ),
234 + // Genuinely no answer from the store. NOT invalid_response, which means the
235 + // store replied and we could not make sense of it: blaming the connection
236 + // there would send people chasing a firewall that is working fine.
237 + 'no_response' => __( 'License server unreachable', $this->domain ),
238 + ];
239 + if ( $issue !== null && isset( $labels[ $issue ] ) ) {
240 + $issueLabel = $labels[ $issue ];
241 + }
242 + }
81 243 if ( strpos( $pathName, $thisPathName ) !== false ) {
82 - $new_links = [
83 - 'settings' =>
84 - sprintf( __( '<a href="admin.php?page=%s_settings">Settings</a>', $this->domain ), $this->prefix ),
85 - 'license' =>
86 - $this->is_registered() ?
87 - ( '<span style="color: #a75bd6;">' . __( 'Pro Version', $this->domain ) . '</span>' ) :
88 - ( $isIssue ? ( sprintf( '<span style="color: #ff3434;">' . __( 'License Issue', $this->domain ), $this->prefix ) . '</span>' ) : ( sprintf( '<span>' . __( '<a target="_blank" href="https://meowapps.com">Get the <u>Pro Version</u></a>', $this->domain ), $this->prefix ) . '</span>' ) ),
89 - ];
244 + // In network admin, handle differently (no settings page available)
245 + if ( is_network_admin() ) {
246 + if ( $this->isPro && !$this->is_registered() ) {
247 + // Show "Register License" link for unregistered Pro plugins
248 + $new_links = [
249 + 'license' => sprintf(
250 + '<a href="#" class="meowapps-network-license-link" data-prefix="%s" data-plugin="%s" style="color: #d63638;">%s</a>',
251 + esc_attr( $this->prefix ),
252 + esc_attr( $this->nice_name_from_file( $this->mainfile ) ),
253 + __( 'Register License', $this->domain )
254 + ),
255 + ];
256 + // Track this plugin for the modal
257 + self::$network_license_plugins[ $this->prefix ] = $this->nice_name_from_file( $this->mainfile );
258 + // Add modal output hook (only once)
259 + if ( !self::$network_license_modal_added ) {
260 + add_action( 'admin_footer', [ __CLASS__, 'output_network_license_modal' ] );
261 + self::$network_license_modal_added = true;
262 + }
263 + }
264 + elseif ( $this->isPro && $this->is_registered() ) {
265 + // Pro plugin is registered
266 + $new_links = [
267 + 'license' => '<span style="color: #a75bd6;">' . __( 'Pro Version', $this->domain ) . '</span>',
268 + ];
269 + }
270 + else {
271 + // Free plugin
272 + $new_links = [
273 + 'license' => sprintf( '<span>' . __( '<a target="_blank" href="https://meowapps.com">Get the <u>Pro Version</u></a>', $this->domain ), $this->prefix ) . '</span>',
274 + ];
275 + }
276 + }
277 + else {
278 + // Regular admin - show settings and license status
279 + $new_links = [
280 + 'settings' =>
281 + sprintf( __( '<a href="admin.php?page=%s_settings">Settings</a>', $this->domain ), $this->prefix ),
282 + 'license' =>
283 + $this->is_registered() ?
284 + ( '<span style="color: #a75bd6;">' . __( 'Pro Version', $this->domain ) . '</span>' ) :
285 + ( $isIssue ? ( '<span style="color: #ff3434;">' . esc_html( $issueLabel ) . '</span>' ) : ( sprintf( '<span>' . __( '<a target="_blank" href="https://meowapps.com">Get the <u>Pro Version</u></a>', $this->domain ), $this->prefix ) . '</span>' ) ),
286 + ];
287 + }
90 288 $links = array_merge( $new_links, $links );
91 289 }
92 290 return $links;
93 291 }
94 292
293 + /**
294 + * Output the network license registration modal.
295 + * Called via admin_footer hook in network admin.
296 + */
297 + public static function output_network_license_modal() {
298 + $rest_url = esc_url( rest_url() );
299 + $nonce = wp_create_nonce( 'wp_rest' );
300 + ?>
301 + <div id="meowapps-network-license-modal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.6); z-index:100000; align-items:center; justify-content:center;">
302 + <div style="background:#fff; padding:24px; border-radius:8px; max-width:450px; width:90%; box-shadow:0 4px 20px rgba(0,0,0,0.3);">
303 + <h2 style="margin:0 0 8px 0; font-size:18px;">Register License</h2>
304 + <p id="meowapps-license-plugin-name" style="margin:0 0 16px 0; color:#666;"></p>
305 + <input type="text" id="meowapps-license-key-input" placeholder="Enter your license key" style="width:100%; padding:10px; font-size:14px; border:1px solid #8c8f94; border-radius:4px; box-sizing:border-box;" />
306 + <p id="meowapps-license-message" style="margin:12px 0 0 0; padding:10px; border-radius:4px; display:none;"></p>
307 + <div style="margin-top:16px; display:flex; gap:10px; justify-content:flex-end;">
308 + <button type="button" id="meowapps-license-cancel" class="button">Cancel</button>
309 + <button type="button" id="meowapps-license-submit" class="button button-primary">Validate & Register</button>
310 + </div>
311 + </div>
312 + </div>
313 + <?php
314 + ob_start();
315 + ?>
316 + (function() {
317 + var modal = document.getElementById('meowapps-network-license-modal');
318 + var input = document.getElementById('meowapps-license-key-input');
319 + var message = document.getElementById('meowapps-license-message');
320 + var pluginName = document.getElementById('meowapps-license-plugin-name');
321 + var submitBtn = document.getElementById('meowapps-license-submit');
322 + var cancelBtn = document.getElementById('meowapps-license-cancel');
323 + var currentPrefix = '';
324 +
325 + function showMessage(text, isError) {
326 + message.textContent = text;
327 + message.style.display = 'block';
328 + message.style.background = isError ? '#fcf0f1' : '#edfaef';
329 + message.style.color = isError ? '#d63638' : '#1e7e34';
330 + message.style.border = '1px solid ' + (isError ? '#d63638' : '#1e7e34');
331 + }
332 +
333 + function hideMessage() {
334 + message.style.display = 'none';
335 + }
336 +
337 + function openModal(prefix, plugin) {
338 + currentPrefix = prefix;
339 + pluginName.textContent = plugin;
340 + input.value = '';
341 + hideMessage();
342 + submitBtn.disabled = false;
343 + submitBtn.textContent = 'Validate & Register';
344 + modal.style.display = 'flex';
345 + input.focus();
346 + }
347 +
348 + function closeModal() {
349 + modal.style.display = 'none';
350 + currentPrefix = '';
351 + }
352 +
353 + // Handle click on "Register License" links
354 + document.addEventListener('click', function(e) {
355 + if (e.target.classList.contains('meowapps-network-license-link')) {
356 + e.preventDefault();
357 + var prefix = e.target.getAttribute('data-prefix');
358 + var plugin = e.target.getAttribute('data-plugin');
359 + openModal(prefix, plugin);
360 + }
361 + });
362 +
363 + // Close modal on cancel or clicking outside
364 + cancelBtn.addEventListener('click', closeModal);
365 + modal.addEventListener('click', function(e) {
366 + if (e.target === modal) closeModal();
367 + });
368 +
369 + // Handle escape key
370 + document.addEventListener('keydown', function(e) {
371 + if (e.key === 'Escape' && modal.style.display === 'flex') {
372 + closeModal();
373 + }
374 + });
375 +
376 + // Handle enter key in input
377 + input.addEventListener('keydown', function(e) {
378 + if (e.key === 'Enter') {
379 + submitBtn.click();
380 + }
381 + });
382 +
383 + // Submit license
384 + submitBtn.addEventListener('click', function() {
385 + var licenseKey = input.value.trim();
386 + if (!licenseKey) {
387 + showMessage('Please enter a license key.', true);
388 + return;
389 + }
390 +
391 + submitBtn.disabled = true;
392 + submitBtn.textContent = 'Validating...';
393 + hideMessage();
394 +
395 + var restUrl = '<?php echo $rest_url; ?>meow-licenser/' + currentPrefix + '/v1/set_license/';
396 +
397 + fetch(restUrl, {
398 + method: 'POST',
399 + headers: {
400 + 'Content-Type': 'application/json',
401 + 'X-WP-Nonce': '<?php echo $nonce; ?>'
402 + },
403 + body: JSON.stringify({ serialKey: licenseKey })
404 + })
405 + .then(function(response) { return response.json(); })
406 + .then(function(data) {
407 + if (data.success && data.data && !data.data.issue) {
408 + showMessage('License registered successfully! Reloading...', false);
409 + setTimeout(function() { location.reload(); }, 1500);
410 + } else {
411 + var errorMsg = 'License validation failed.';
412 + if (data.data && data.data.issue) {
413 + errorMsg = 'License issue: ' + data.data.issue;
414 + }
415 + showMessage(errorMsg, true);
416 + submitBtn.disabled = false;
417 + submitBtn.textContent = 'Validate & Register';
418 + }
419 + })
420 + .catch(function(error) {
421 + showMessage('Error: ' + error.message, true);
422 + submitBtn.disabled = false;
423 + submitBtn.textContent = 'Validate & Register';
424 + });
425 + });
426 + })();
427 + <?php
428 + wp_print_inline_script_tag( ob_get_clean() );
429 + }
430 +
95 431 public function request_verify_ssl() {
96 432 return get_option( 'force_sslverify', false );
97 433 }
98 434
@@ -108,9 +444,12 @@
108 444 return $file;
109 445 }
110 446
111 447 public function admin_notices_licensed_free() {
112 - if ( isset( $_POST[$this->prefix . '_reset_sub'] ) ) {
448 + // Verify the nonce before removing the license from a POST request (CSRF protection).
449 + if ( isset( $_POST[$this->prefix . '_reset_sub'] )
450 + && isset( $_POST[ $this->prefix . '_reset_sub_nonce' ] )
451 + && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ $this->prefix . '_reset_sub_nonce' ] ) ), $this->prefix . '_reset_sub' ) ) {
113 452 delete_option( $this->prefix . '_pro_serial' );
114 453 delete_option( $this->prefix . '_license' );
115 454 return;
116 455 }
@@ -120,9 +459,9 @@
120 459 $this->nice_name_from_file( $this->mainfile )
121 460 );
122 461 $html .= '<p>
123 462 <form method="post" action="">
124 - <input type="hidden" name="' . $this->prefix . '_reset_sub" value="true">
463 + <input type="hidden" name="' . $this->prefix . '_reset_sub" value="true"><input type="hidden" name="' . $this->prefix . '_reset_sub_nonce" value="' . esc_attr( wp_create_nonce( $this->prefix . '_reset_sub' ) ) . '">
125 464 <input type="submit" name="submit" id="submit" class="button" value="'
126 465 . __( 'Remove the license', $this->domain ) . '">
127 466 </form>
128 467 </p>';
@@ -132,19 +471,23 @@
132 471
133 472 public function admin_menu_start() {
134 473 // Hide the admin if user doesn't like Meow much
135 474 if ( get_option( 'meowapps_hide_meowapps', false ) ) {
136 - register_setting( 'general', 'meowapps_hide_meowapps' );
475 + register_setting( 'general', 'meowapps_hide_meowapps', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ] );
137 476 add_settings_field( 'meowapps_hide_ads', 'Meow Apps Menu', [ $this, 'meowapps_hide_dashboard_callback' ], 'general' );
138 477 return;
139 478 }
140 479
141 - // Create standard menu if it does not already exist
480 + // Create standard menu if it does not already exist.
481 + // The cat logo is injected as an <img> inside the menu title (rather than passed as
482 + // the $icon_url argument) so the original SVG fills are preserved — passing it via
483 + // $icon_url makes WordPress add the .svg class and the admin color scheme strips
484 + // the colors to a single fill.
142 485 global $submenu;
143 486 if ( !isset( $submenu[ 'meowapps-main-menu' ] ) ) {
144 487 add_menu_page(
145 488 'Meow Apps',
146 - '<img alt="Meow Apps" style="width: 22px; margin-left: -30px; position: absolute; margin-top: -0px;" src="' . MeowCommon_Admin::$logo . '" />Meow Apps',
489 + '<img alt="Meow Apps" class="meowapps-menu-icon" src="' . MeowKit_MWCODE_Admin::$logo . '" />Meow Apps',
147 490 'manage_options',
148 491 'meowapps-main-menu',
149 492 [ $this, 'admin_meow_apps' ],
150 493 '',
@@ -159,15 +502,46 @@
159 502 [ $this, 'admin_meow_apps' ]
160 503 );
161 504 }
162 505
163 - // Add CSS to hide the default icon
164 - add_action( 'admin_head', function () {
165 - echo '<style>
166 - #toplevel_page_meowapps-main-menu .wp-menu-image {
167 - display: none;
168 - }
169 - </style>';
506 + // Position the cat icon so it sits in the standard icon column in both the
507 + // expanded and the collapsed (folded) sidebar.
508 + //
509 + // The image lives inside the .wp-menu-name title (where the original code
510 + // put it) so the <img> renders with its native SVG fills. When the sidebar
511 + // is collapsed, WP hides .wp-menu-name (and everything inside it), so a
512 + // small JS snippet below clones the same <img> into the .wp-menu-image
513 + // slot — which WP keeps visible in both states. We use a real <img>
514 + // (not a background-image) on purpose: at small sizes WP's admin renders
515 + // background-image SVGs noticeably lighter than the equivalent <img>,
516 + // and we want the colored cat in both states.
517 + //
518 + // The !important rules also override an older "display: none" style that
519 + // previous-generation Meow Apps common copies inject for .wp-menu-image;
520 + // if any of them load first, ours still wins.
521 + // Enqueue the menu-icon CSS/JS through the assets API instead of echoing inline tags.
522 + add_action( 'admin_enqueue_scripts', function () {
523 + $css = '
524 + #toplevel_page_meowapps-main-menu .meowapps-menu-icon { width: 20px; height: auto; position: absolute; margin-left: -28px; margin-top: 3px; }
525 + #toplevel_page_meowapps-main-menu .wp-menu-image { display: block !important; position: relative; }
526 + #toplevel_page_meowapps-main-menu .wp-menu-image::before { display: none !important; }
527 + #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; }
528 + body.folded #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded { display: block; }
529 + body.folded #toplevel_page_meowapps-main-menu .meowapps-menu-icon { display: none; }
530 + @media only screen and (max-width: 960px) {
531 + body.auto-fold #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded { display: block; }
532 + body.auto-fold #toplevel_page_meowapps-main-menu .meowapps-menu-icon { display: none; }
533 + }';
534 + wp_add_inline_style( 'admin-menu', $css );
535 +
536 + // Clone the in-title cat icon into the .wp-menu-image slot (CSS shows it only when folded).
537 + $js = 'document.addEventListener("DOMContentLoaded", function () {'
538 + . 'var li = document.getElementById("toplevel_page_meowapps-main-menu"); if ( !li ) { return; }'
539 + . 'var src = li.querySelector(".meowapps-menu-icon"); var slot = li.querySelector(".wp-menu-image");'
540 + . 'if ( !src || !slot || slot.querySelector(".meowapps-menu-icon-folded") ) { return; }'
541 + . 'var clone = src.cloneNode(); clone.className = "meowapps-menu-icon-folded"; clone.removeAttribute("style"); slot.appendChild(clone);'
542 + . '});';
543 + wp_add_inline_script( 'common', $js );
170 544 } );
171 545 }
172 546
173 547 public function meowapps_hide_dashboard_callback() {
@@ -173,9 +547,9 @@
173 547 public function meowapps_hide_dashboard_callback() {
174 548 $html = '<input type="checkbox" id="meowapps_hide_meowapps" name="meowapps_hide_meowapps" value="1" ' .
175 549 checked( 1, get_option( 'meowapps_hide_meowapps' ), false ) . '/>';
176 550 $html .= __( '<label>Hide <b>Meow Apps</b> Menu</label><br /><small>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.<br /><b>Once activated, an option will be added in your General settings to display it again.</b></small>', $this->domain );
177 - echo MeowCommon_Helpers::wp_kses( $html );
551 + echo MeowKit_MWCODE_Helpers::wp_kses( $html );
178 552 }
179 553
180 554 public function is_registered() {
181 555 $is_registered = apply_filters( $this->prefix . '_meowapps_is_registered', false, $this->prefix );
@@ -208,10 +582,205 @@
208 582 public function admin_footer_text( $current ) {
209 583 return sprintf(
210 584 // translators: %1$s is the version of the interface; %2$s is a file path.
211 585 __( 'Thanks for using <a href="https://meowapps.com">Meow Apps</a>! This is the Meow Admin %1$s <br /><i>Loaded from %2$s </i>', $this->domain ),
212 - MeowCommon_Admin::$version,
586 + MeowKit_MWCODE_Admin::$version,
213 587 __FILE__
214 588 );
589 + }
590 +
591 + /**
592 + * Renders a promo banner on WordPress 7's Connectors page when AI Engine
593 + * isn't installed. Kept self-contained so the common library stays simple:
594 + * no new file, no REST endpoint, dismissal persists in localStorage.
595 + */
596 + public function maybe_render_wpai_promo() {
597 + // WordPress 7+ only.
598 + if ( ! class_exists( 'WP_Connector_Registry' ) ) {
599 + return;
600 + }
601 + // If AI Engine is installed, its own Connectors banner takes over.
602 + if ( class_exists( 'Meow_MWAI_Core' ) ) {
603 + return;
604 + }
605 + // Another Meow Apps plugin's common copy may have rendered already.
606 + if ( defined( 'MEOWAPPS_WPAI_PROMO_RENDERED' ) ) {
607 + return;
608 + }
609 + // Gate on the Connectors screen. The hook suffix differs between the
610 + // direct file (`options-connectors.php`) and the menu-page variant.
611 + $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
612 + $id = $screen ? $screen->id : ( isset( $GLOBALS['hook_suffix'] ) ? $GLOBALS['hook_suffix'] : '' );
613 + $targets = array( 'options-connectors', 'options-connectors.php', 'settings_page_options-connectors-wp-admin' );
614 + if ( ! in_array( $id, $targets, true ) ) {
615 + return;
616 + }
617 + define( 'MEOWAPPS_WPAI_PROMO_RENDERED', true );
618 +
619 + // Install button → WordPress's own plugin-install search page, pre-
620 + // filtered for AI Engine. One click from there to install. This is the
621 + // most reliable path: no custom nonces, native progress UI, native
622 + // filesystem credential prompt if needed.
623 + $can_install = current_user_can( 'install_plugins' );
624 + $install_url = $can_install
625 + ? self_admin_url( 'plugin-install.php?tab=search&type=term&s=AI+Engine' )
626 + : 'https://wordpress.org/plugins/ai-engine/';
627 + $wporg_url = 'https://wordpress.org/plugins/ai-engine/';
628 + $learn_url = 'https://meowapps.com/wordpress-7-ai-engine-gateway/';
629 +
630 + // Title is split so "AI Engine" can carry an anchor to wp.org. Keeping
631 + // the pieces as data (not one HTML string) avoids escaping surprises and
632 + // keeps the translation unit stable.
633 + $payload = array(
634 + 'titleBefore' => __( 'Highly recommended: Let ', 'meowapps' ),
635 + 'titleLink' => __( 'AI Engine', 'meowapps' ),
636 + 'titleAfter' => __( ' handle your connections.', 'meowapps' ),
637 + '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' ),
638 + 'install' => __( 'Install AI Engine', 'meowapps' ),
639 + 'learn' => __( 'Learn more', 'meowapps' ),
640 + 'dismiss' => __( 'Dismiss', 'meowapps' ),
641 + 'installUrl' => $install_url,
642 + 'wporgUrl' => $wporg_url,
643 + 'learnUrl' => $learn_url,
644 + );
645 + wp_register_style( 'meowapps-common-inline', false );
646 + wp_enqueue_style( 'meowapps-common-inline' );
647 + ob_start();
648 + ?>
649 + .meowapps-wpai-promo {
650 + margin: 0 0 16px; padding: 14px 18px;
651 + display: flex; align-items: flex-start; gap: 14px;
652 + background: #f0f4ff; border: 1px solid #d6deff; border-radius: 4px;
653 + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
654 + font-size: 13px; line-height: 1.45; color: #1e1e1e;
655 + }
656 + .meowapps-wpai-promo-icon {
657 + width: 32px; height: 32px; border-radius: 50%;
658 + background: #2f5fff; color: #fff;
659 + display: flex; align-items: center; justify-content: center;
660 + flex-shrink: 0;
661 + margin-top: 1px;
662 + }
663 + .meowapps-wpai-promo-icon svg { width: 18px; height: 18px; display: block; }
664 + .meowapps-wpai-promo-body {
665 + flex: 1; min-width: 0;
666 + display: flex; flex-direction: column; gap: 10px;
667 + }
668 + .meowapps-wpai-promo-text strong { font-weight: 700; display: block; margin-bottom: 3px; }
669 + .meowapps-wpai-promo-text span { color: #50575e; }
670 + .meowapps-wpai-promo-titlelink {
671 + color: #2f5fff; text-decoration: none; border-bottom: 1px dashed #2f5fff;
672 + }
673 + .meowapps-wpai-promo-titlelink:hover { color: #2448cc; border-bottom-color: #2448cc; }
674 + .meowapps-wpai-promo-actions {
675 + display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
676 + }
677 + .meowapps-wpai-promo-btn {
678 + appearance: none; border: 1px solid transparent; border-radius: 4px;
679 + padding: 7px 16px; font: inherit; font-size: 12.5px; font-weight: 600;
680 + cursor: pointer; text-decoration: none;
681 + transition: background 0.12s ease, border-color 0.12s ease;
682 + }
683 + .meowapps-wpai-promo-btn-primary { background: #2f5fff; color: #fff; }
684 + .meowapps-wpai-promo-btn-primary:hover { background: #2448cc; color: #fff; }
685 + .meowapps-wpai-promo-btn-secondary { background: #7c3aed; color: #fff; }
686 + .meowapps-wpai-promo-btn-secondary:hover { background: #6527c9; color: #fff; }
687 + .meowapps-wpai-promo-btn-dismiss {
688 + margin-left: auto;
689 + background: transparent; color: #6b7280;
690 + font-weight: 500; padding: 7px 10px;
691 + }
692 + .meowapps-wpai-promo-btn-dismiss:hover { color: #1e1e1e; background: rgba(0,0,0,0.04); }
693 + <?php
694 + wp_add_inline_style( 'meowapps-common-inline', ob_get_clean() );
695 +
696 + wp_register_script( 'meowapps-common-inline', false, array(), false, true );
697 + wp_enqueue_script( 'meowapps-common-inline' );
698 + ob_start();
699 + ?>
700 + (function () {
701 + var D = <?php echo wp_json_encode( $payload ); ?>;
702 + try { if (localStorage.getItem('meowapps-wpai-promo-dismissed') === '1') return; } catch (e) {}
703 +
704 + function build() {
705 + var host = document.createElement('div');
706 + host.className = 'meowapps-wpai-promo';
707 + host.setAttribute('role', 'status');
708 + var iconSvg = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>';
709 + host.innerHTML = [
710 + '<span class="meowapps-wpai-promo-icon">', iconSvg, '</span>',
711 + '<div class="meowapps-wpai-promo-body">',
712 + '<div class="meowapps-wpai-promo-text">',
713 + '<strong></strong> <span class="meowapps-wpai-promo-sub"></span>',
714 + '</div>',
715 + '<div class="meowapps-wpai-promo-actions"></div>',
716 + '</div>'
717 + ].join('');
718 + // Title carries an anchor on "AI Engine" → wp.org plugin page.
719 + var strong = host.querySelector('strong');
720 + strong.appendChild(document.createTextNode(D.titleBefore));
721 + var tLink = document.createElement('a');
722 + tLink.href = D.wporgUrl;
723 + tLink.target = '_blank';
724 + tLink.rel = 'noopener noreferrer';
725 + tLink.className = 'meowapps-wpai-promo-titlelink';
726 + tLink.textContent = D.titleLink;
727 + strong.appendChild(tLink);
728 + strong.appendChild(document.createTextNode(D.titleAfter));
729 + host.querySelector('.meowapps-wpai-promo-sub').textContent = D.sub;
730 + var actions = host.querySelector('.meowapps-wpai-promo-actions');
731 +
732 + function link(label, cls, href, newTab) {
733 + var a = document.createElement('a');
734 + a.className = 'meowapps-wpai-promo-btn ' + cls;
735 + a.textContent = label;
736 + a.href = href;
737 + if (newTab) { a.target = '_blank'; a.rel = 'noopener noreferrer'; }
738 + actions.appendChild(a);
739 + return a;
740 + }
741 +
742 + // Install → WordPress's plugin-install search page, pre-filtered.
743 + // User lands on a familiar screen with AI Engine as the top hit and
744 + // can install with the native WordPress UX.
745 + link(D.install, 'meowapps-wpai-promo-btn-primary', D.installUrl, false);
746 + link(D.learn, 'meowapps-wpai-promo-btn-secondary', D.learnUrl, true);
747 +
748 + var d = document.createElement('button');
749 + d.type = 'button';
750 + d.className = 'meowapps-wpai-promo-btn meowapps-wpai-promo-btn-dismiss';
751 + d.textContent = D.dismiss;
752 + d.addEventListener('click', function () {
753 + try { localStorage.setItem('meowapps-wpai-promo-dismissed', '1'); } catch (e) {}
754 + if (host.parentNode) host.parentNode.removeChild(host);
755 + });
756 + actions.appendChild(d);
757 + return host;
758 + }
759 +
760 + function ensure() {
761 + if (document.querySelector('.meowapps-wpai-promo')) return;
762 + var page = document.querySelector('.connectors-page');
763 + if (page) { page.insertBefore(build(), page.firstChild); return; }
764 + var header = document.querySelector('.boot-layout__stage header');
765 + if (header && header.parentNode) {
766 + header.parentNode.insertBefore(build(), header.nextSibling);
767 + }
768 + }
769 +
770 + var tries = 0;
771 + var iv = setInterval(function () {
772 + ensure();
773 + if (document.querySelector('.meowapps-wpai-promo') || ++tries > 40) clearInterval(iv);
774 + }, 120);
775 +
776 + var app = document.getElementById('options-connectors-wp-admin-app')
777 + || document.getElementById('options-connectors-app');
778 + if (app && 'MutationObserver' in window) {
779 + new MutationObserver(ensure).observe(app, { childList: true, subtree: true });
780 + }
781 + })();
782 + <?php
783 + wp_add_inline_script( 'meowapps-common-inline', ob_get_clean() );
215 784 }
216 785 }
217 786 }