PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.7.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.7.3
3.7.3 3.7.2 3.7.1 3.7.0 3.6.9 3.6.8 3.6.7 3.6.6 3.6.4 3.6.5 3.6.3 3.6.2 3.6.1 3.6.0 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / common / admin.php
ai-engine / common Last commit date
admin.php 5 days ago helpers.php 8 months ago issues.php 9 months ago news.php 1 month ago ratings.php 1 month ago releases.txt 2 years ago rest.php 1 week ago
admin.php
787 lines
1 <?php
2
3 if ( !class_exists( 'MeowKit_MWAI_Admin' ) ) {
4
5 class MeowKit_MWAI_Admin {
6 public static $loaded = false;
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 = [];
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
29 public $prefix; // prefix used for actions, filters (mfrh)
30 public $mainfile; // plugin main file (media-file-renamer.php)
31 public $domain; // domain used for translation (media-file-renamer)
32 public $isPro = false;
33
34 // Store constructor params that affect per-instance setup
35 private $disableReview = false;
36
37 public static $logo = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNjQiIGhlaWdodD0iNDYiIHZpZXdCb3g9IjAgMCA2NCA0NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8ZyBjbGlwUGF0aD0idXJsKCNjbGlwMF8zMTBfMjI5KSI+CiAgICA8cGF0aCBkPSJNNjQgMzAuNjQwOEM2NCAyNy43OTg1IDYwLjA4MTYgMjUuODMwMyA1NS44Mjk4IDI1LjgzMDNDNTQuODU5MyAyNS44MzAzIDUzLjkzMTEgMjUuOTMzIDUzLjA3NiAyNi4xMjUzQzQ5Ljg4NjUgMTkuMDc5IDQxLjY1MzkgMTMuMDg1MyAzMi4wMDAyIDEzLjA4NTNDMzAuODMzNyAxMy4wODUzIDI5LjY4ODEgMTMuMTcyNyAyOC41Njk4IDEzLjMzOTJDMjcuMjA2OSAxMC4zMDc2IDIyLjY3NjIgMi40MzQyNiAxMS41OTU0IDAuMDgzMDA2NEMxMS4wNDkxIC0wLjAzMjc0NTYgMTAuNDk0NiAwLjI0MDU3OCAxMC4yNTkgMC43NDY5QzguODU5MTMgMy43NTYwOCA0Ljc0MjQ3IDE0LjQxMTYgMTAuMjQwMyAyNS45OTMxQzkuNTgxNjUgMjUuODg2NCA4Ljg4NzUxIDI1LjgzMDMgOC4xNzAyMiAyNS44MzAzQzMuOTE4MzkgMjUuODMwMyAwIDI3Ljc5ODUgMCAzMC42NDA4QzAgMzMuNDgzIDMuOTE4MzkgMzUuMjI3MiA4LjE3MDIyIDM1LjIyNzJDOC43MTEyNyAzNS4yMjcyIDkuMjM5MjUgMzUuMTk4OCA5Ljc0ODkzIDM1LjE0MzVDOS40MzYwMiAzNS4yNjY0IDkuMTIyNzUgMzUuNDA3NSA4LjgxMTcxIDM1LjU2NzdDNS42OTM4OCAzNy4xNzA3IDMuOTY4OCA0MC4wMzEyIDQuOTU5MDQgNDEuOTU2OEM1Ljk0ODkgNDMuODgyNCA5LjI3OTIgNDQuMTQ0MiAxMi4zOTcgNDIuNTQxMkMxMy4wNDY0IDQyLjIwNzQgMTMuNjM0OCA0MS44MTkgMTQuMTUxNiA0MS4zOTZDMTguMjYyNyA0NC40OTY3IDI0LjcyODMgNDUuOTgwOSAzMS45OTk4IDQ1Ljk4MDlDMzkuMjcxMyA0NS45ODA5IDQ1LjczNyA0NC40OTY3IDQ5Ljg0OCA0MS4zOTZDNTAuMzY0NCA0MS44MTkgNTAuOTUzMyA0Mi4yMDc0IDUxLjYwMjYgNDIuNTQxMkM1NC43MjA0IDQ0LjE0NDIgNTguMDUwMyA0My44ODI0IDU5LjA0MDYgNDEuOTU2OEM2MC4wMzA1IDQwLjAzMTIgNTguMzA1NyAzNy4xNzA3IDU1LjE4NzkgMzUuNTY3N0M1NC44NzYxIDM1LjQwNzUgNTQuNTYyMSAzNS4yNjY3IDU0LjI0ODUgMzUuMTQzNUM1NC43NTg5IDM1LjE5ODggNTUuMjg3NiAzNS4yMjc1IDU1LjgyOTQgMzUuMjI3NUM2MC4wODEyIDM1LjIyNzUgNjMuOTk5NiAzMy40ODM0IDYzLjk5OTYgMzAuNjQxMUw2NCAzMC42NDA4WiIgZmlsbD0id2hpdGUiLz4KICAgIDxwYXRoIGQ9Ik0yMi4yMjkzIDM2Ljc0NDNDMjYuNTkzNSAzNi43NDQzIDMwLjEzMTQgMzMuMjA2NCAzMC4xMzE0IDI4Ljg0MjJDMzAuMTMxNCAyNC40NzggMjYuNTkzNSAyMC45NDAxIDIyLjIyOTMgMjAuOTQwMUMxNy44NjUxIDIwLjk0MDEgMTQuMzI3MSAyNC40NzggMTQuMzI3MSAyOC44NDIyQzE0LjMyNzEgMzMuMjA2NCAxNy44NjUxIDM2Ljc0NDMgMjIuMjI5MyAzNi43NDQzWiIgZmlsbD0iIzAwRTI4RSIvPgogICAgPHBhdGggZD0iTTIyLjI2NTUgMzMuMTM2MUMyMy41MDIyIDMzLjEzNjEgMjQuNTA0NyAzMS4yODA1IDI0LjUwNDcgMjguOTkxNUMyNC41MDQ3IDI2LjcwMjQgMjMuNTAyMiAyNC44NDY4IDIyLjI2NTUgMjQuODQ2OEMyMS4wMjg4IDI0Ljg0NjggMjAuMDI2MiAyNi43MDI0IDIwLjAyNjIgMjguOTkxNUMyMC4wMjYyIDMxLjI4MDUgMjEuMDI4OCAzMy4xMzYxIDIyLjI2NTUgMzMuMTM2MVoiIGZpbGw9IiMzQzZFOEIiLz4KICAgIDxwYXRoIGQ9Ik0zMS45OTk4IDM3LjkxNTZDMzMuNDIzNyAzNy45MTU2IDM0LjU3ODEgMzcuMzQwOSAzNC41NzgxIDM2LjYzMTlDMzQuNTc4MSAzNS45MjI5IDMzLjQyMzcgMzUuMzQ4MSAzMS45OTk4IDM1LjM0ODFDMzAuNTc1OCAzNS4zNDgxIDI5LjQyMTUgMzUuOTIyOSAyOS40MjE1IDM2LjYzMTlDMjkuNDIxNSAzNy4zNDA5IDMwLjU3NTggMzcuOTE1NiAzMS45OTk4IDM3LjkxNTZaIiBmaWxsPSIjRkY5NDkzIi8+CiAgICA8cGF0aCBkPSJNNTQuMjUwMyAzNS4xMDU4QzU0Ljc2IDM1LjE2MTEgNTUuMjg3OSAzNS4xODk0IDU1LjgyOSAzNS4xODk0QzYwLjA4MDggMzUuMTg5NCA2My45OTkyIDMzLjQ0NTMgNjMuOTk5MiAzMC42MDNDNjMuOTk5MiAyNy43NjA4IDYwLjA4MDggMjUuNzkyNiA1NS44MjkgMjUuNzkyNkM1NS4xMTE3IDI1Ljc5MjYgNTQuNDE3NiAyNS44NDkgNTMuNzU4NSAyNS45NTU4QzU5LjI1NjcgMTQuMzc0MiA1NS4xMzk3IDMuNzE4NzIgNTMuNzQwMiAwLjcwOTU0NkM1My41MDQ2IDAuMjAzMjI1IDUyLjk1MDEgLTAuMDcwMDk5MSA1Mi40MDM4IDAuMDQ1NjUyOUM0MS4zMjMgMi4zOTY5MSAzNi43OTIzIDEwLjI3MDcgMzUuNDI5OCAxMy4zMDE1QzM0LjQ1NDEgMTMuMTU2NiAzMy40NTc5IDEzLjA3MTEgMzIuNDQ1MiAxMy4wNTE3QzMxLjI3NDMgMjAuMDMzIDI4Ljk2NTYgNDMuOTM2NSA1NC4zNDM2IDM1LjE0MzlDNTQuMzEyMyAzNS4xMzEyIDU0LjI4MTMgMzUuMTE4MSA1NC4yNDk5IDM1LjEwNThINTQuMjUwM1oiIGZpbGw9IiMyQjlERkYiLz4KICAgIDxwYXRoIGQ9Ik00MS43MzQyIDMzLjEzNjFDNDIuOTcwOSAzMy4xMzYxIDQzLjk3MzUgMzEuMjgwNSA0My45NzM1IDI4Ljk5MTVDNDMuOTczNSAyNi43MDI0IDQyLjk3MDkgMjQuODQ2OCA0MS43MzQyIDI0Ljg0NjhDNDAuNDk3NSAyNC44NDY4IDM5LjQ5NSAyNi43MDI0IDM5LjQ5NSAyOC45OTE1QzM5LjQ5NSAzMS4yODA1IDQwLjQ5NzUgMzMuMTM2MSA0MS43MzQyIDMzLjEzNjFaIiBmaWxsPSIjM0M2RThCIi8+CiAgPC9nPgogIDxkZWZzPgogICAgPGNsaXBQYXRoIGlkPSJjbGlwMF8zMTBfMjI5Ij4KICAgICAgPHJlY3Qgd2lkdGg9IjY0IiBoZWlnaHQ9IjQ1Ljk2MTciIGZpbGw9IndoaXRlIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDAuMDE5MTY1KSIvPgogICAgPC9jbGlwUGF0aD4KICA8L2RlZnM+Cjwvc3ZnPgo=';
38
39 public function __construct( $prefix, $mainfile, $domain, $isPro = false, $disableReview = false, $freeOnly = false ) {
40
41 // ALWAYS set instance properties first - these are needed regardless of when setup runs
42 $this->prefix = $prefix;
43 $this->mainfile = $mainfile;
44 $this->domain = $domain;
45 $this->isPro = $isPro;
46 $this->disableReview = $disableReview;
47
48 if ( is_admin() ) {
49
50 // Skip AJAX and REST requests to avoid unnecessary processing
51 if ( MeowKit_MWAI_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)
70 $license = get_option( $this->prefix . '_license', '' );
71 if ( !empty( $license ) && !$this->isPro ) {
72 add_action( 'admin_notices', [ $this, 'admin_notices_licensed_free' ] );
73 }
74 }
75
76 // ALWAYS register these filters (they work at any time)
77 add_filter( 'plugin_row_meta', [ $this, 'custom_plugin_row_meta' ], 10, 2 );
78 add_filter( 'edd_sl_api_request_verify_ssl', [ $this, 'request_verify_ssl' ], 10, 0 );
79 }
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_MWAI_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_MWAI_Admin::$loaded ) {
149 // Check for potential issues with WordPress install, other plugins, etc.
150 new MeowKit_MWAI_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_MWAI_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_MWAI_Ratings( $this->prefix, $this->mainfile, $this->domain );
174 }
175 new MeowKit_MWAI_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 */
188 public function is_user_admin() {
189 // Defensive check (should never fail if called from run_admin_setup)
190 if ( !function_exists( 'current_user_can' ) || !function_exists( 'wp_get_current_user' ) ) {
191 return false;
192 }
193 return current_user_can( 'manage_options' );
194 }
195
196 public function custom_plugin_row_meta( $links, $file ) {
197 $path = pathinfo( $file );
198 $pathName = basename( $path['dirname'] );
199 $thisPath = pathinfo( $this->mainfile );
200 $thisPathName = basename( $thisPath['dirname'] );
201 $isActive = is_plugin_active( $file );
202 if ( !$isActive ) {
203 return $links;
204 }
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 }
243 if ( strpos( $pathName, $thisPathName ) !== false ) {
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 }
288 $links = array_merge( $new_links, $links );
289 }
290 return $links;
291 }
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
431 public function request_verify_ssl() {
432 return get_option( 'force_sslverify', false );
433 }
434
435 public function nice_name_from_file( $file ) {
436 $info = pathinfo( $file );
437 if ( !empty( $info ) ) {
438 if ( $info['filename'] == 'wplr-sync' ) {
439 return 'WP/LR Sync';
440 }
441 $info['filename'] = str_replace( '-', ' ', $info['filename'] );
442 $file = ucwords( $info['filename'] );
443 }
444 return $file;
445 }
446
447 public function admin_notices_licensed_free() {
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' ) ) {
452 delete_option( $this->prefix . '_pro_serial' );
453 delete_option( $this->prefix . '_license' );
454 return;
455 }
456 $html = '<div class="notice notice-error">';
457 $html .= sprintf(
458 __( '<p>It looks like you are using the free version of the plugin (<b>%s</b>) 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, <b>please download it again</b> from the <a target="_blank" href="https://meowapps.com">Meow Store</a>. If you wish to continue using the free version and clear this message, click on this button.', $this->domain ),
459 $this->nice_name_from_file( $this->mainfile )
460 );
461 $html .= '<p>
462 <form method="post" action="">
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' ) ) . '">
464 <input type="submit" name="submit" id="submit" class="button" value="'
465 . __( 'Remove the license', $this->domain ) . '">
466 </form>
467 </p>';
468 $html .= '</div>';
469 wp_kses_post( $html );
470 }
471
472 public function admin_menu_start() {
473 // Hide the admin if user doesn't like Meow much
474 if ( get_option( 'meowapps_hide_meowapps', false ) ) {
475 register_setting( 'general', 'meowapps_hide_meowapps', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ] );
476 add_settings_field( 'meowapps_hide_ads', 'Meow Apps Menu', [ $this, 'meowapps_hide_dashboard_callback' ], 'general' );
477 return;
478 }
479
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.
485 global $submenu;
486 if ( !isset( $submenu[ 'meowapps-main-menu' ] ) ) {
487 add_menu_page(
488 'Meow Apps',
489 '<img alt="Meow Apps" class="meowapps-menu-icon" src="' . MeowKit_MWAI_Admin::$logo . '" />Meow Apps',
490 'manage_options',
491 'meowapps-main-menu',
492 [ $this, 'admin_meow_apps' ],
493 '',
494 82
495 );
496 add_submenu_page(
497 'meowapps-main-menu',
498 __( 'Dashboard', $this->domain ),
499 __( 'Dashboard', $this->domain ),
500 'manage_options',
501 'meowapps-main-menu',
502 [ $this, 'admin_meow_apps' ]
503 );
504 }
505
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 );
544 } );
545 }
546
547 public function meowapps_hide_dashboard_callback() {
548 $html = '<input type="checkbox" id="meowapps_hide_meowapps" name="meowapps_hide_meowapps" value="1" ' .
549 checked( 1, get_option( 'meowapps_hide_meowapps' ), false ) . '/>';
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 );
551 echo MeowKit_MWAI_Helpers::wp_kses( $html );
552 }
553
554 public function is_registered() {
555 $is_registered = apply_filters( $this->prefix . '_meowapps_is_registered', false, $this->prefix );
556 return $is_registered;
557 }
558
559 public function get_phpinfo() {
560 if ( !$this->is_user_admin() || !function_exists( 'phpinfo' ) ) {
561 return;
562 }
563 ob_start();
564 // phpcs:disable WordPress.PHP.DevelopmentFunctions
565 phpinfo( INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES );
566 // phpcs:enable
567 $html = ob_get_contents();
568 ob_end_clean();
569 $html = preg_replace( '%^.*<body>(.*)</body>.*$%ms', '$1', $html );
570 return $html;
571 }
572
573 public function admin_meow_apps() {
574 $html = "<div id='meow-common-dashboard'></div>";
575 $html .= "<div style='height: 0; width: 0; overflow: hidden;' id='meow-common-phpinfo'>";
576 $html .= $this->get_phpinfo();
577 $html .= '</div>';
578 $html = preg_replace( "/<img[^>]+\>/i", '', $html );
579 echo wp_kses_post( $html );
580 }
581
582 public function admin_footer_text( $current ) {
583 return sprintf(
584 // translators: %1$s is the version of the interface; %2$s is a file path.
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 ),
586 MeowKit_MWAI_Admin::$version,
587 __FILE__
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() );
784 }
785 }
786 }
787