PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.5.8
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.5.8
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 14 hours ago helpers.php 6 months ago issues.php 7 months ago news.php 14 hours ago ratings.php 14 hours ago releases.txt 2 years ago rest.php 1 week ago
admin.php
750 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 if ( strpos( $pathName, $thisPathName ) !== false ) {
207 // In network admin, handle differently (no settings page available)
208 if ( is_network_admin() ) {
209 if ( $this->isPro && !$this->is_registered() ) {
210 // Show "Register License" link for unregistered Pro plugins
211 $new_links = [
212 'license' => sprintf(
213 '<a href="#" class="meowapps-network-license-link" data-prefix="%s" data-plugin="%s" style="color: #d63638;">%s</a>',
214 esc_attr( $this->prefix ),
215 esc_attr( $this->nice_name_from_file( $this->mainfile ) ),
216 __( 'Register License', $this->domain )
217 ),
218 ];
219 // Track this plugin for the modal
220 self::$network_license_plugins[ $this->prefix ] = $this->nice_name_from_file( $this->mainfile );
221 // Add modal output hook (only once)
222 if ( !self::$network_license_modal_added ) {
223 add_action( 'admin_footer', [ __CLASS__, 'output_network_license_modal' ] );
224 self::$network_license_modal_added = true;
225 }
226 }
227 elseif ( $this->isPro && $this->is_registered() ) {
228 // Pro plugin is registered
229 $new_links = [
230 'license' => '<span style="color: #a75bd6;">' . __( 'Pro Version', $this->domain ) . '</span>',
231 ];
232 }
233 else {
234 // Free plugin
235 $new_links = [
236 'license' => sprintf( '<span>' . __( '<a target="_blank" href="https://meowapps.com">Get the <u>Pro Version</u></a>', $this->domain ), $this->prefix ) . '</span>',
237 ];
238 }
239 }
240 else {
241 // Regular admin - show settings and license status
242 $new_links = [
243 'settings' =>
244 sprintf( __( '<a href="admin.php?page=%s_settings">Settings</a>', $this->domain ), $this->prefix ),
245 'license' =>
246 $this->is_registered() ?
247 ( '<span style="color: #a75bd6;">' . __( 'Pro Version', $this->domain ) . '</span>' ) :
248 ( $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>' ) ),
249 ];
250 }
251 $links = array_merge( $new_links, $links );
252 }
253 return $links;
254 }
255
256 /**
257 * Output the network license registration modal.
258 * Called via admin_footer hook in network admin.
259 */
260 public static function output_network_license_modal() {
261 $rest_url = esc_url( rest_url() );
262 $nonce = wp_create_nonce( 'wp_rest' );
263 ?>
264 <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;">
265 <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);">
266 <h2 style="margin:0 0 8px 0; font-size:18px;">Register License</h2>
267 <p id="meowapps-license-plugin-name" style="margin:0 0 16px 0; color:#666;"></p>
268 <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;" />
269 <p id="meowapps-license-message" style="margin:12px 0 0 0; padding:10px; border-radius:4px; display:none;"></p>
270 <div style="margin-top:16px; display:flex; gap:10px; justify-content:flex-end;">
271 <button type="button" id="meowapps-license-cancel" class="button">Cancel</button>
272 <button type="button" id="meowapps-license-submit" class="button button-primary">Validate & Register</button>
273 </div>
274 </div>
275 </div>
276 <?php
277 ob_start();
278 ?>
279 (function() {
280 var modal = document.getElementById('meowapps-network-license-modal');
281 var input = document.getElementById('meowapps-license-key-input');
282 var message = document.getElementById('meowapps-license-message');
283 var pluginName = document.getElementById('meowapps-license-plugin-name');
284 var submitBtn = document.getElementById('meowapps-license-submit');
285 var cancelBtn = document.getElementById('meowapps-license-cancel');
286 var currentPrefix = '';
287
288 function showMessage(text, isError) {
289 message.textContent = text;
290 message.style.display = 'block';
291 message.style.background = isError ? '#fcf0f1' : '#edfaef';
292 message.style.color = isError ? '#d63638' : '#1e7e34';
293 message.style.border = '1px solid ' + (isError ? '#d63638' : '#1e7e34');
294 }
295
296 function hideMessage() {
297 message.style.display = 'none';
298 }
299
300 function openModal(prefix, plugin) {
301 currentPrefix = prefix;
302 pluginName.textContent = plugin;
303 input.value = '';
304 hideMessage();
305 submitBtn.disabled = false;
306 submitBtn.textContent = 'Validate & Register';
307 modal.style.display = 'flex';
308 input.focus();
309 }
310
311 function closeModal() {
312 modal.style.display = 'none';
313 currentPrefix = '';
314 }
315
316 // Handle click on "Register License" links
317 document.addEventListener('click', function(e) {
318 if (e.target.classList.contains('meowapps-network-license-link')) {
319 e.preventDefault();
320 var prefix = e.target.getAttribute('data-prefix');
321 var plugin = e.target.getAttribute('data-plugin');
322 openModal(prefix, plugin);
323 }
324 });
325
326 // Close modal on cancel or clicking outside
327 cancelBtn.addEventListener('click', closeModal);
328 modal.addEventListener('click', function(e) {
329 if (e.target === modal) closeModal();
330 });
331
332 // Handle escape key
333 document.addEventListener('keydown', function(e) {
334 if (e.key === 'Escape' && modal.style.display === 'flex') {
335 closeModal();
336 }
337 });
338
339 // Handle enter key in input
340 input.addEventListener('keydown', function(e) {
341 if (e.key === 'Enter') {
342 submitBtn.click();
343 }
344 });
345
346 // Submit license
347 submitBtn.addEventListener('click', function() {
348 var licenseKey = input.value.trim();
349 if (!licenseKey) {
350 showMessage('Please enter a license key.', true);
351 return;
352 }
353
354 submitBtn.disabled = true;
355 submitBtn.textContent = 'Validating...';
356 hideMessage();
357
358 var restUrl = '<?php echo $rest_url; ?>meow-licenser/' + currentPrefix + '/v1/set_license/';
359
360 fetch(restUrl, {
361 method: 'POST',
362 headers: {
363 'Content-Type': 'application/json',
364 'X-WP-Nonce': '<?php echo $nonce; ?>'
365 },
366 body: JSON.stringify({ serialKey: licenseKey })
367 })
368 .then(function(response) { return response.json(); })
369 .then(function(data) {
370 if (data.success && data.data && !data.data.issue) {
371 showMessage('License registered successfully! Reloading...', false);
372 setTimeout(function() { location.reload(); }, 1500);
373 } else {
374 var errorMsg = 'License validation failed.';
375 if (data.data && data.data.issue) {
376 errorMsg = 'License issue: ' + data.data.issue;
377 }
378 showMessage(errorMsg, true);
379 submitBtn.disabled = false;
380 submitBtn.textContent = 'Validate & Register';
381 }
382 })
383 .catch(function(error) {
384 showMessage('Error: ' + error.message, true);
385 submitBtn.disabled = false;
386 submitBtn.textContent = 'Validate & Register';
387 });
388 });
389 })();
390 <?php
391 wp_print_inline_script_tag( ob_get_clean() );
392 }
393
394 public function request_verify_ssl() {
395 return get_option( 'force_sslverify', false );
396 }
397
398 public function nice_name_from_file( $file ) {
399 $info = pathinfo( $file );
400 if ( !empty( $info ) ) {
401 if ( $info['filename'] == 'wplr-sync' ) {
402 return 'WP/LR Sync';
403 }
404 $info['filename'] = str_replace( '-', ' ', $info['filename'] );
405 $file = ucwords( $info['filename'] );
406 }
407 return $file;
408 }
409
410 public function admin_notices_licensed_free() {
411 // Verify the nonce before removing the license from a POST request (CSRF protection).
412 if ( isset( $_POST[$this->prefix . '_reset_sub'] )
413 && isset( $_POST[ $this->prefix . '_reset_sub_nonce' ] )
414 && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ $this->prefix . '_reset_sub_nonce' ] ) ), $this->prefix . '_reset_sub' ) ) {
415 delete_option( $this->prefix . '_pro_serial' );
416 delete_option( $this->prefix . '_license' );
417 return;
418 }
419 $html = '<div class="notice notice-error">';
420 $html .= sprintf(
421 __( '<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 ),
422 $this->nice_name_from_file( $this->mainfile )
423 );
424 $html .= '<p>
425 <form method="post" action="">
426 <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' ) ) . '">
427 <input type="submit" name="submit" id="submit" class="button" value="'
428 . __( 'Remove the license', $this->domain ) . '">
429 </form>
430 </p>';
431 $html .= '</div>';
432 wp_kses_post( $html );
433 }
434
435 public function admin_menu_start() {
436 // Hide the admin if user doesn't like Meow much
437 if ( get_option( 'meowapps_hide_meowapps', false ) ) {
438 register_setting( 'general', 'meowapps_hide_meowapps', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ] );
439 add_settings_field( 'meowapps_hide_ads', 'Meow Apps Menu', [ $this, 'meowapps_hide_dashboard_callback' ], 'general' );
440 return;
441 }
442
443 // Create standard menu if it does not already exist.
444 // The cat logo is injected as an <img> inside the menu title (rather than passed as
445 // the $icon_url argument) so the original SVG fills are preserved — passing it via
446 // $icon_url makes WordPress add the .svg class and the admin color scheme strips
447 // the colors to a single fill.
448 global $submenu;
449 if ( !isset( $submenu[ 'meowapps-main-menu' ] ) ) {
450 add_menu_page(
451 'Meow Apps',
452 '<img alt="Meow Apps" class="meowapps-menu-icon" src="' . MeowKit_MWAI_Admin::$logo . '" />Meow Apps',
453 'manage_options',
454 'meowapps-main-menu',
455 [ $this, 'admin_meow_apps' ],
456 '',
457 82
458 );
459 add_submenu_page(
460 'meowapps-main-menu',
461 __( 'Dashboard', $this->domain ),
462 __( 'Dashboard', $this->domain ),
463 'manage_options',
464 'meowapps-main-menu',
465 [ $this, 'admin_meow_apps' ]
466 );
467 }
468
469 // Position the cat icon so it sits in the standard icon column in both the
470 // expanded and the collapsed (folded) sidebar.
471 //
472 // The image lives inside the .wp-menu-name title (where the original code
473 // put it) so the <img> renders with its native SVG fills. When the sidebar
474 // is collapsed, WP hides .wp-menu-name (and everything inside it), so a
475 // small JS snippet below clones the same <img> into the .wp-menu-image
476 // slot — which WP keeps visible in both states. We use a real <img>
477 // (not a background-image) on purpose: at small sizes WP's admin renders
478 // background-image SVGs noticeably lighter than the equivalent <img>,
479 // and we want the colored cat in both states.
480 //
481 // The !important rules also override an older "display: none" style that
482 // previous-generation Meow Apps common copies inject for .wp-menu-image;
483 // if any of them load first, ours still wins.
484 // Enqueue the menu-icon CSS/JS through the assets API instead of echoing inline tags.
485 add_action( 'admin_enqueue_scripts', function () {
486 $css = '
487 #toplevel_page_meowapps-main-menu .meowapps-menu-icon { width: 20px; height: auto; position: absolute; margin-left: -28px; margin-top: 3px; }
488 #toplevel_page_meowapps-main-menu .wp-menu-image { display: block !important; position: relative; }
489 #toplevel_page_meowapps-main-menu .wp-menu-image::before { display: none !important; }
490 #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; }
491 body.folded #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded { display: block; }
492 body.folded #toplevel_page_meowapps-main-menu .meowapps-menu-icon { display: none; }
493 @media only screen and (max-width: 960px) {
494 body.auto-fold #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded { display: block; }
495 body.auto-fold #toplevel_page_meowapps-main-menu .meowapps-menu-icon { display: none; }
496 }';
497 wp_add_inline_style( 'admin-menu', $css );
498
499 // Clone the in-title cat icon into the .wp-menu-image slot (CSS shows it only when folded).
500 $js = 'document.addEventListener("DOMContentLoaded", function () {'
501 . 'var li = document.getElementById("toplevel_page_meowapps-main-menu"); if ( !li ) { return; }'
502 . 'var src = li.querySelector(".meowapps-menu-icon"); var slot = li.querySelector(".wp-menu-image");'
503 . 'if ( !src || !slot || slot.querySelector(".meowapps-menu-icon-folded") ) { return; }'
504 . 'var clone = src.cloneNode(); clone.className = "meowapps-menu-icon-folded"; clone.removeAttribute("style"); slot.appendChild(clone);'
505 . '});';
506 wp_add_inline_script( 'common', $js );
507 } );
508 }
509
510 public function meowapps_hide_dashboard_callback() {
511 $html = '<input type="checkbox" id="meowapps_hide_meowapps" name="meowapps_hide_meowapps" value="1" ' .
512 checked( 1, get_option( 'meowapps_hide_meowapps' ), false ) . '/>';
513 $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 );
514 echo MeowKit_MWAI_Helpers::wp_kses( $html );
515 }
516
517 public function is_registered() {
518 $is_registered = apply_filters( $this->prefix . '_meowapps_is_registered', false, $this->prefix );
519 return $is_registered;
520 }
521
522 public function get_phpinfo() {
523 if ( !$this->is_user_admin() || !function_exists( 'phpinfo' ) ) {
524 return;
525 }
526 ob_start();
527 // phpcs:disable WordPress.PHP.DevelopmentFunctions
528 phpinfo( INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES );
529 // phpcs:enable
530 $html = ob_get_contents();
531 ob_end_clean();
532 $html = preg_replace( '%^.*<body>(.*)</body>.*$%ms', '$1', $html );
533 return $html;
534 }
535
536 public function admin_meow_apps() {
537 $html = "<div id='meow-common-dashboard'></div>";
538 $html .= "<div style='height: 0; width: 0; overflow: hidden;' id='meow-common-phpinfo'>";
539 $html .= $this->get_phpinfo();
540 $html .= '</div>';
541 $html = preg_replace( "/<img[^>]+\>/i", '', $html );
542 echo wp_kses_post( $html );
543 }
544
545 public function admin_footer_text( $current ) {
546 return sprintf(
547 // translators: %1$s is the version of the interface; %2$s is a file path.
548 __( '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 ),
549 MeowKit_MWAI_Admin::$version,
550 __FILE__
551 );
552 }
553
554 /**
555 * Renders a promo banner on WordPress 7's Connectors page when AI Engine
556 * isn't installed. Kept self-contained so the common library stays simple:
557 * no new file, no REST endpoint, dismissal persists in localStorage.
558 */
559 public function maybe_render_wpai_promo() {
560 // WordPress 7+ only.
561 if ( ! class_exists( 'WP_Connector_Registry' ) ) {
562 return;
563 }
564 // If AI Engine is installed, its own Connectors banner takes over.
565 if ( class_exists( 'Meow_MWAI_Core' ) ) {
566 return;
567 }
568 // Another Meow Apps plugin's common copy may have rendered already.
569 if ( defined( 'MEOWAPPS_WPAI_PROMO_RENDERED' ) ) {
570 return;
571 }
572 // Gate on the Connectors screen. The hook suffix differs between the
573 // direct file (`options-connectors.php`) and the menu-page variant.
574 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
575 $id = $screen ? $screen->id : ( isset( $GLOBALS['hook_suffix'] ) ? $GLOBALS['hook_suffix'] : '' );
576 $targets = array( 'options-connectors', 'options-connectors.php', 'settings_page_options-connectors-wp-admin' );
577 if ( ! in_array( $id, $targets, true ) ) {
578 return;
579 }
580 define( 'MEOWAPPS_WPAI_PROMO_RENDERED', true );
581
582 // Install button → WordPress's own plugin-install search page, pre-
583 // filtered for AI Engine. One click from there to install. This is the
584 // most reliable path: no custom nonces, native progress UI, native
585 // filesystem credential prompt if needed.
586 $can_install = current_user_can( 'install_plugins' );
587 $install_url = $can_install
588 ? self_admin_url( 'plugin-install.php?tab=search&type=term&s=AI+Engine' )
589 : 'https://wordpress.org/plugins/ai-engine/';
590 $wporg_url = 'https://wordpress.org/plugins/ai-engine/';
591 $learn_url = 'https://meowapps.com/wordpress-7-ai-engine-gateway/';
592
593 // Title is split so "AI Engine" can carry an anchor to wp.org. Keeping
594 // the pieces as data (not one HTML string) avoids escaping surprises and
595 // keeps the translation unit stable.
596 $payload = array(
597 'titleBefore' => __( 'Highly recommended: Let ', 'meowapps' ),
598 'titleLink' => __( 'AI Engine', 'meowapps' ),
599 'titleAfter' => __( ' handle your connections.', 'meowapps' ),
600 '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' ),
601 'install' => __( 'Install AI Engine', 'meowapps' ),
602 'learn' => __( 'Learn more', 'meowapps' ),
603 'dismiss' => __( 'Dismiss', 'meowapps' ),
604 'installUrl' => $install_url,
605 'wporgUrl' => $wporg_url,
606 'learnUrl' => $learn_url,
607 );
608 wp_register_style( 'meowapps-common-inline', false );
609 wp_enqueue_style( 'meowapps-common-inline' );
610 ob_start();
611 ?>
612 .meowapps-wpai-promo {
613 margin: 0 0 16px; padding: 14px 18px;
614 display: flex; align-items: flex-start; gap: 14px;
615 background: #f0f4ff; border: 1px solid #d6deff; border-radius: 4px;
616 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
617 font-size: 13px; line-height: 1.45; color: #1e1e1e;
618 }
619 .meowapps-wpai-promo-icon {
620 width: 32px; height: 32px; border-radius: 50%;
621 background: #2f5fff; color: #fff;
622 display: flex; align-items: center; justify-content: center;
623 flex-shrink: 0;
624 margin-top: 1px;
625 }
626 .meowapps-wpai-promo-icon svg { width: 18px; height: 18px; display: block; }
627 .meowapps-wpai-promo-body {
628 flex: 1; min-width: 0;
629 display: flex; flex-direction: column; gap: 10px;
630 }
631 .meowapps-wpai-promo-text strong { font-weight: 700; display: block; margin-bottom: 3px; }
632 .meowapps-wpai-promo-text span { color: #50575e; }
633 .meowapps-wpai-promo-titlelink {
634 color: #2f5fff; text-decoration: none; border-bottom: 1px dashed #2f5fff;
635 }
636 .meowapps-wpai-promo-titlelink:hover { color: #2448cc; border-bottom-color: #2448cc; }
637 .meowapps-wpai-promo-actions {
638 display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
639 }
640 .meowapps-wpai-promo-btn {
641 appearance: none; border: 1px solid transparent; border-radius: 4px;
642 padding: 7px 16px; font: inherit; font-size: 12.5px; font-weight: 600;
643 cursor: pointer; text-decoration: none;
644 transition: background 0.12s ease, border-color 0.12s ease;
645 }
646 .meowapps-wpai-promo-btn-primary { background: #2f5fff; color: #fff; }
647 .meowapps-wpai-promo-btn-primary:hover { background: #2448cc; color: #fff; }
648 .meowapps-wpai-promo-btn-secondary { background: #7c3aed; color: #fff; }
649 .meowapps-wpai-promo-btn-secondary:hover { background: #6527c9; color: #fff; }
650 .meowapps-wpai-promo-btn-dismiss {
651 margin-left: auto;
652 background: transparent; color: #6b7280;
653 font-weight: 500; padding: 7px 10px;
654 }
655 .meowapps-wpai-promo-btn-dismiss:hover { color: #1e1e1e; background: rgba(0,0,0,0.04); }
656 <?php
657 wp_add_inline_style( 'meowapps-common-inline', ob_get_clean() );
658
659 wp_register_script( 'meowapps-common-inline', false, array(), false, true );
660 wp_enqueue_script( 'meowapps-common-inline' );
661 ob_start();
662 ?>
663 (function () {
664 var D = <?php echo wp_json_encode( $payload ); ?>;
665 try { if (localStorage.getItem('meowapps-wpai-promo-dismissed') === '1') return; } catch (e) {}
666
667 function build() {
668 var host = document.createElement('div');
669 host.className = 'meowapps-wpai-promo';
670 host.setAttribute('role', 'status');
671 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>';
672 host.innerHTML = [
673 '<span class="meowapps-wpai-promo-icon">', iconSvg, '</span>',
674 '<div class="meowapps-wpai-promo-body">',
675 '<div class="meowapps-wpai-promo-text">',
676 '<strong></strong> <span class="meowapps-wpai-promo-sub"></span>',
677 '</div>',
678 '<div class="meowapps-wpai-promo-actions"></div>',
679 '</div>'
680 ].join('');
681 // Title carries an anchor on "AI Engine" → wp.org plugin page.
682 var strong = host.querySelector('strong');
683 strong.appendChild(document.createTextNode(D.titleBefore));
684 var tLink = document.createElement('a');
685 tLink.href = D.wporgUrl;
686 tLink.target = '_blank';
687 tLink.rel = 'noopener noreferrer';
688 tLink.className = 'meowapps-wpai-promo-titlelink';
689 tLink.textContent = D.titleLink;
690 strong.appendChild(tLink);
691 strong.appendChild(document.createTextNode(D.titleAfter));
692 host.querySelector('.meowapps-wpai-promo-sub').textContent = D.sub;
693 var actions = host.querySelector('.meowapps-wpai-promo-actions');
694
695 function link(label, cls, href, newTab) {
696 var a = document.createElement('a');
697 a.className = 'meowapps-wpai-promo-btn ' + cls;
698 a.textContent = label;
699 a.href = href;
700 if (newTab) { a.target = '_blank'; a.rel = 'noopener noreferrer'; }
701 actions.appendChild(a);
702 return a;
703 }
704
705 // Install → WordPress's plugin-install search page, pre-filtered.
706 // User lands on a familiar screen with AI Engine as the top hit and
707 // can install with the native WordPress UX.
708 link(D.install, 'meowapps-wpai-promo-btn-primary', D.installUrl, false);
709 link(D.learn, 'meowapps-wpai-promo-btn-secondary', D.learnUrl, true);
710
711 var d = document.createElement('button');
712 d.type = 'button';
713 d.className = 'meowapps-wpai-promo-btn meowapps-wpai-promo-btn-dismiss';
714 d.textContent = D.dismiss;
715 d.addEventListener('click', function () {
716 try { localStorage.setItem('meowapps-wpai-promo-dismissed', '1'); } catch (e) {}
717 if (host.parentNode) host.parentNode.removeChild(host);
718 });
719 actions.appendChild(d);
720 return host;
721 }
722
723 function ensure() {
724 if (document.querySelector('.meowapps-wpai-promo')) return;
725 var page = document.querySelector('.connectors-page');
726 if (page) { page.insertBefore(build(), page.firstChild); return; }
727 var header = document.querySelector('.boot-layout__stage header');
728 if (header && header.parentNode) {
729 header.parentNode.insertBefore(build(), header.nextSibling);
730 }
731 }
732
733 var tries = 0;
734 var iv = setInterval(function () {
735 ensure();
736 if (document.querySelector('.meowapps-wpai-promo') || ++tries > 40) clearInterval(iv);
737 }, 120);
738
739 var app = document.getElementById('options-connectors-wp-admin-app')
740 || document.getElementById('options-connectors-app');
741 if (app && 'MutationObserver' in window) {
742 new MutationObserver(ensure).observe(app, { childList: true, subtree: true });
743 }
744 })();
745 <?php
746 wp_add_inline_script( 'meowapps-common-inline', ob_get_clean() );
747 }
748 }
749 }
750