PluginProbe ʕ •ᴥ•ʔ
Media Cleaner: Clean your WordPress! / trunk
Media Cleaner: Clean your WordPress! vtrunk
7.1.1 7.1.0 7.0.9 7.0.8 trunk 3.6.8 3.6.9 3.7.0 3.8.0 3.9.0 4.0.0 4.0.2 4.0.4 4.0.6 4.0.7 4.1.0 4.2.0 4.2.2 4.2.3 4.2.4 4.2.5 4.4.0 4.4.2 4.4.4 4.4.6 4.4.7 4.4.8 4.5.0 4.5.4 4.5.6 4.5.7 4.5.8 4.6.2 4.6.3 4.8.0 4.8.4 5.0.0 5.0.1 5.1.0 5.1.1 5.1.3 5.2.0 5.2.1 5.2.4 5.4.0 5.4.1 5.4.2 5.4.3 5.4.4 5.4.5 5.4.6 5.4.9 5.5.0 5.5.1 5.5.2 5.5.3 5.5.4 5.5.7 5.5.8 5.6.1 5.6.2 5.6.3 5.6.4 6.0.1 6.0.2 6.0.3 6.0.4 6.0.5 6.0.6 6.0.7 6.0.8 6.0.9 6.1.2 6.1.3 6.1.4 6.1.5 6.1.6 6.1.7 6.1.8 6.1.9 6.2.0 6.2.1 6.2.3 6.2.4 6.2.5 6.2.6 6.2.7 6.2.8 6.3.0 6.3.1 6.3.2 6.3.4 6.3.5 6.3.7 6.3.8 6.3.9 6.4.0 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.4.7 6.4.8 6.4.9 6.5.0 6.5.1 6.5.2 6.5.3 6.5.4 6.5.5 6.5.6 6.5.7 6.5.8 6.5.9 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.6.6 6.6.7 6.6.8 6.6.9 6.7.0 6.7.1 6.7.2 6.7.3 6.7.4 6.7.5 6.7.6 6.7.7 6.7.8 6.7.9 6.8.0 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.8.6 6.8.7 6.8.8 6.8.9 6.9.0 6.9.1 6.9.2 6.9.3 6.9.4 6.9.5 6.9.6 6.9.7 6.9.8 6.9.9 7.0.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.0.6 7.0.7
media-cleaner / common / admin.php
media-cleaner / common Last commit date
admin.php 1 month ago helpers.php 5 months ago issues.php 7 months ago news.php 7 months ago ratings.php 7 months ago releases.txt 2 years ago rest.php 1 month ago
admin.php
776 lines
1 <?php
2
3 if ( !class_exists( 'MeowKit_WPMC_Admin' ) ) {
4
5 class MeowKit_WPMC_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_WPMC_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_WPMC_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_WPMC_Admin::$loaded ) {
149 // Check for potential issues with WordPress install, other plugins, etc.
150 new MeowKit_WPMC_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_footer', [ $this, 'maybe_render_wpai_promo' ] );
165
166 MeowKit_WPMC_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_WPMC_Ratings( $this->prefix, $this->mainfile, $this->domain );
174 }
175 new MeowKit_WPMC_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 <script>
277 (function() {
278 var modal = document.getElementById('meowapps-network-license-modal');
279 var input = document.getElementById('meowapps-license-key-input');
280 var message = document.getElementById('meowapps-license-message');
281 var pluginName = document.getElementById('meowapps-license-plugin-name');
282 var submitBtn = document.getElementById('meowapps-license-submit');
283 var cancelBtn = document.getElementById('meowapps-license-cancel');
284 var currentPrefix = '';
285
286 function showMessage(text, isError) {
287 message.textContent = text;
288 message.style.display = 'block';
289 message.style.background = isError ? '#fcf0f1' : '#edfaef';
290 message.style.color = isError ? '#d63638' : '#1e7e34';
291 message.style.border = '1px solid ' + (isError ? '#d63638' : '#1e7e34');
292 }
293
294 function hideMessage() {
295 message.style.display = 'none';
296 }
297
298 function openModal(prefix, plugin) {
299 currentPrefix = prefix;
300 pluginName.textContent = plugin;
301 input.value = '';
302 hideMessage();
303 submitBtn.disabled = false;
304 submitBtn.textContent = 'Validate & Register';
305 modal.style.display = 'flex';
306 input.focus();
307 }
308
309 function closeModal() {
310 modal.style.display = 'none';
311 currentPrefix = '';
312 }
313
314 // Handle click on "Register License" links
315 document.addEventListener('click', function(e) {
316 if (e.target.classList.contains('meowapps-network-license-link')) {
317 e.preventDefault();
318 var prefix = e.target.getAttribute('data-prefix');
319 var plugin = e.target.getAttribute('data-plugin');
320 openModal(prefix, plugin);
321 }
322 });
323
324 // Close modal on cancel or clicking outside
325 cancelBtn.addEventListener('click', closeModal);
326 modal.addEventListener('click', function(e) {
327 if (e.target === modal) closeModal();
328 });
329
330 // Handle escape key
331 document.addEventListener('keydown', function(e) {
332 if (e.key === 'Escape' && modal.style.display === 'flex') {
333 closeModal();
334 }
335 });
336
337 // Handle enter key in input
338 input.addEventListener('keydown', function(e) {
339 if (e.key === 'Enter') {
340 submitBtn.click();
341 }
342 });
343
344 // Submit license
345 submitBtn.addEventListener('click', function() {
346 var licenseKey = input.value.trim();
347 if (!licenseKey) {
348 showMessage('Please enter a license key.', true);
349 return;
350 }
351
352 submitBtn.disabled = true;
353 submitBtn.textContent = 'Validating...';
354 hideMessage();
355
356 var restUrl = '<?php echo $rest_url; ?>meow-licenser/' + currentPrefix + '/v1/set_license/';
357
358 fetch(restUrl, {
359 method: 'POST',
360 headers: {
361 'Content-Type': 'application/json',
362 'X-WP-Nonce': '<?php echo $nonce; ?>'
363 },
364 body: JSON.stringify({ serialKey: licenseKey })
365 })
366 .then(function(response) { return response.json(); })
367 .then(function(data) {
368 if (data.success && data.data && !data.data.issue) {
369 showMessage('License registered successfully! Reloading...', false);
370 setTimeout(function() { location.reload(); }, 1500);
371 } else {
372 var errorMsg = 'License validation failed.';
373 if (data.data && data.data.issue) {
374 errorMsg = 'License issue: ' + data.data.issue;
375 }
376 showMessage(errorMsg, true);
377 submitBtn.disabled = false;
378 submitBtn.textContent = 'Validate & Register';
379 }
380 })
381 .catch(function(error) {
382 showMessage('Error: ' + error.message, true);
383 submitBtn.disabled = false;
384 submitBtn.textContent = 'Validate & Register';
385 });
386 });
387 })();
388 </script>
389 <?php
390 }
391
392 public function request_verify_ssl() {
393 return get_option( 'force_sslverify', false );
394 }
395
396 public function nice_name_from_file( $file ) {
397 $info = pathinfo( $file );
398 if ( !empty( $info ) ) {
399 if ( $info['filename'] == 'wplr-sync' ) {
400 return 'WP/LR Sync';
401 }
402 $info['filename'] = str_replace( '-', ' ', $info['filename'] );
403 $file = ucwords( $info['filename'] );
404 }
405 return $file;
406 }
407
408 public function admin_notices_licensed_free() {
409 if ( isset( $_POST[$this->prefix . '_reset_sub'] ) ) {
410 delete_option( $this->prefix . '_pro_serial' );
411 delete_option( $this->prefix . '_license' );
412 return;
413 }
414 $html = '<div class="notice notice-error">';
415 $html .= sprintf(
416 __( '<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 ),
417 $this->nice_name_from_file( $this->mainfile )
418 );
419 $html .= '<p>
420 <form method="post" action="">
421 <input type="hidden" name="' . $this->prefix . '_reset_sub" value="true">
422 <input type="submit" name="submit" id="submit" class="button" value="'
423 . __( 'Remove the license', $this->domain ) . '">
424 </form>
425 </p>';
426 $html .= '</div>';
427 wp_kses_post( $html );
428 }
429
430 public function admin_menu_start() {
431 // Hide the admin if user doesn't like Meow much
432 if ( get_option( 'meowapps_hide_meowapps', false ) ) {
433 register_setting( 'general', 'meowapps_hide_meowapps' );
434 add_settings_field( 'meowapps_hide_ads', 'Meow Apps Menu', [ $this, 'meowapps_hide_dashboard_callback' ], 'general' );
435 return;
436 }
437
438 // Create standard menu if it does not already exist.
439 // The cat logo is injected as an <img> inside the menu title (rather than passed as
440 // the $icon_url argument) so the original SVG fills are preserved — passing it via
441 // $icon_url makes WordPress add the .svg class and the admin color scheme strips
442 // the colors to a single fill.
443 global $submenu;
444 if ( !isset( $submenu[ 'meowapps-main-menu' ] ) ) {
445 add_menu_page(
446 'Meow Apps',
447 '<img alt="Meow Apps" class="meowapps-menu-icon" src="' . MeowKit_WPMC_Admin::$logo . '" />Meow Apps',
448 'manage_options',
449 'meowapps-main-menu',
450 [ $this, 'admin_meow_apps' ],
451 '',
452 82
453 );
454 add_submenu_page(
455 'meowapps-main-menu',
456 __( 'Dashboard', $this->domain ),
457 __( 'Dashboard', $this->domain ),
458 'manage_options',
459 'meowapps-main-menu',
460 [ $this, 'admin_meow_apps' ]
461 );
462 }
463
464 // Position the cat icon so it sits in the standard icon column in both the
465 // expanded and the collapsed (folded) sidebar.
466 //
467 // The image lives inside the .wp-menu-name title (where the original code
468 // put it) so the <img> renders with its native SVG fills. When the sidebar
469 // is collapsed, WP hides .wp-menu-name (and everything inside it), so a
470 // small JS snippet below clones the same <img> into the .wp-menu-image
471 // slot — which WP keeps visible in both states. We use a real <img>
472 // (not a background-image) on purpose: at small sizes WP's admin renders
473 // background-image SVGs noticeably lighter than the equivalent <img>,
474 // and we want the colored cat in both states.
475 //
476 // The !important rules also override an older "display: none" style that
477 // previous-generation Meow Apps common copies inject for .wp-menu-image;
478 // if any of them load first, ours still wins.
479 add_action( 'admin_head', function () {
480 echo '<style>
481 #toplevel_page_meowapps-main-menu .meowapps-menu-icon {
482 width: 20px;
483 height: auto;
484 position: absolute;
485 margin-left: -28px;
486 margin-top: 3px;
487 }
488 #toplevel_page_meowapps-main-menu .wp-menu-image {
489 display: block !important;
490 position: relative;
491 }
492 #toplevel_page_meowapps-main-menu .wp-menu-image::before {
493 display: none !important;
494 }
495 #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded {
496 display: none;
497 position: absolute;
498 top: 50%;
499 left: 50%;
500 transform: translate(-50%, calc(-50% - 3px));
501 width: 20px;
502 height: auto;
503 }
504 body.folded #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded {
505 display: block;
506 }
507 body.folded #toplevel_page_meowapps-main-menu .meowapps-menu-icon {
508 display: none;
509 }
510 @media only screen and (max-width: 960px) {
511 body.auto-fold #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded {
512 display: block;
513 }
514 body.auto-fold #toplevel_page_meowapps-main-menu .meowapps-menu-icon {
515 display: none;
516 }
517 }
518 </style>';
519 }, 999 );
520
521 // Inject a clone of the in-title cat icon into the .wp-menu-image slot.
522 // CSS above shows it only when the sidebar is folded; in the normal
523 // expanded state the .wp-menu-name <img> stays the visible icon.
524 add_action( 'admin_footer', function () {
525 ?>
526 <script>
527 ( function () {
528 var li = document.getElementById( 'toplevel_page_meowapps-main-menu' );
529 if ( !li ) { return; }
530 var src = li.querySelector( '.meowapps-menu-icon' );
531 var slot = li.querySelector( '.wp-menu-image' );
532 if ( !src || !slot || slot.querySelector( '.meowapps-menu-icon-folded' ) ) { return; }
533 var clone = src.cloneNode();
534 clone.className = 'meowapps-menu-icon-folded';
535 clone.removeAttribute( 'style' );
536 slot.appendChild( clone );
537 } )();
538 </script>
539 <?php
540 } );
541 }
542
543 public function meowapps_hide_dashboard_callback() {
544 $html = '<input type="checkbox" id="meowapps_hide_meowapps" name="meowapps_hide_meowapps" value="1" ' .
545 checked( 1, get_option( 'meowapps_hide_meowapps' ), false ) . '/>';
546 $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 );
547 echo MeowKit_WPMC_Helpers::wp_kses( $html );
548 }
549
550 public function is_registered() {
551 $is_registered = apply_filters( $this->prefix . '_meowapps_is_registered', false, $this->prefix );
552 return $is_registered;
553 }
554
555 public function get_phpinfo() {
556 if ( !$this->is_user_admin() || !function_exists( 'phpinfo' ) ) {
557 return;
558 }
559 ob_start();
560 // phpcs:disable WordPress.PHP.DevelopmentFunctions
561 phpinfo( INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES );
562 // phpcs:enable
563 $html = ob_get_contents();
564 ob_end_clean();
565 $html = preg_replace( '%^.*<body>(.*)</body>.*$%ms', '$1', $html );
566 return $html;
567 }
568
569 public function admin_meow_apps() {
570 $html = "<div id='meow-common-dashboard'></div>";
571 $html .= "<div style='height: 0; width: 0; overflow: hidden;' id='meow-common-phpinfo'>";
572 $html .= $this->get_phpinfo();
573 $html .= '</div>';
574 $html = preg_replace( "/<img[^>]+\>/i", '', $html );
575 echo wp_kses_post( $html );
576 }
577
578 public function admin_footer_text( $current ) {
579 return sprintf(
580 // translators: %1$s is the version of the interface; %2$s is a file path.
581 __( '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 ),
582 MeowKit_WPMC_Admin::$version,
583 __FILE__
584 );
585 }
586
587 /**
588 * Renders a promo banner on WordPress 7's Connectors page when AI Engine
589 * isn't installed. Kept self-contained so the common library stays simple:
590 * no new file, no REST endpoint, dismissal persists in localStorage.
591 */
592 public function maybe_render_wpai_promo() {
593 // WordPress 7+ only.
594 if ( ! class_exists( 'WP_Connector_Registry' ) ) {
595 return;
596 }
597 // If AI Engine is installed, its own Connectors banner takes over.
598 if ( class_exists( 'Meow_MWAI_Core' ) ) {
599 return;
600 }
601 // Another Meow Apps plugin's common copy may have rendered already.
602 if ( defined( 'MEOWAPPS_WPAI_PROMO_RENDERED' ) ) {
603 return;
604 }
605 // Gate on the Connectors screen. The hook suffix differs between the
606 // direct file (`options-connectors.php`) and the menu-page variant.
607 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
608 $id = $screen ? $screen->id : ( isset( $GLOBALS['hook_suffix'] ) ? $GLOBALS['hook_suffix'] : '' );
609 $targets = array( 'options-connectors', 'options-connectors.php', 'settings_page_options-connectors-wp-admin' );
610 if ( ! in_array( $id, $targets, true ) ) {
611 return;
612 }
613 define( 'MEOWAPPS_WPAI_PROMO_RENDERED', true );
614
615 // Install button → WordPress's own plugin-install search page, pre-
616 // filtered for AI Engine. One click from there to install. This is the
617 // most reliable path: no custom nonces, native progress UI, native
618 // filesystem credential prompt if needed.
619 $can_install = current_user_can( 'install_plugins' );
620 $install_url = $can_install
621 ? self_admin_url( 'plugin-install.php?tab=search&type=term&s=AI+Engine' )
622 : 'https://wordpress.org/plugins/ai-engine/';
623 $wporg_url = 'https://wordpress.org/plugins/ai-engine/';
624 $learn_url = 'https://meowapps.com/wordpress-7-ai-engine-gateway/';
625
626 // Title is split so "AI Engine" can carry an anchor to wp.org. Keeping
627 // the pieces as data (not one HTML string) avoids escaping surprises and
628 // keeps the translation unit stable.
629 $payload = array(
630 'titleBefore' => __( 'Highly recommended: Let ', 'meowapps' ),
631 'titleLink' => __( 'AI Engine', 'meowapps' ),
632 'titleAfter' => __( ' handle your connections.', 'meowapps' ),
633 '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' ),
634 'install' => __( 'Install AI Engine', 'meowapps' ),
635 'learn' => __( 'Learn more', 'meowapps' ),
636 'dismiss' => __( 'Dismiss', 'meowapps' ),
637 'installUrl' => $install_url,
638 'wporgUrl' => $wporg_url,
639 'learnUrl' => $learn_url,
640 );
641 ?>
642 <style>
643 .meowapps-wpai-promo {
644 margin: 0 0 16px; padding: 14px 18px;
645 display: flex; align-items: flex-start; gap: 14px;
646 background: #f0f4ff; border: 1px solid #d6deff; border-radius: 4px;
647 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
648 font-size: 13px; line-height: 1.45; color: #1e1e1e;
649 }
650 .meowapps-wpai-promo-icon {
651 width: 32px; height: 32px; border-radius: 50%;
652 background: #2f5fff; color: #fff;
653 display: flex; align-items: center; justify-content: center;
654 flex-shrink: 0;
655 margin-top: 1px;
656 }
657 .meowapps-wpai-promo-icon svg { width: 18px; height: 18px; display: block; }
658 .meowapps-wpai-promo-body {
659 flex: 1; min-width: 0;
660 display: flex; flex-direction: column; gap: 10px;
661 }
662 .meowapps-wpai-promo-text strong { font-weight: 700; display: block; margin-bottom: 3px; }
663 .meowapps-wpai-promo-text span { color: #50575e; }
664 .meowapps-wpai-promo-titlelink {
665 color: #2f5fff; text-decoration: none; border-bottom: 1px dashed #2f5fff;
666 }
667 .meowapps-wpai-promo-titlelink:hover { color: #2448cc; border-bottom-color: #2448cc; }
668 .meowapps-wpai-promo-actions {
669 display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
670 }
671 .meowapps-wpai-promo-btn {
672 appearance: none; border: 1px solid transparent; border-radius: 4px;
673 padding: 7px 16px; font: inherit; font-size: 12.5px; font-weight: 600;
674 cursor: pointer; text-decoration: none;
675 transition: background 0.12s ease, border-color 0.12s ease;
676 }
677 .meowapps-wpai-promo-btn-primary { background: #2f5fff; color: #fff; }
678 .meowapps-wpai-promo-btn-primary:hover { background: #2448cc; color: #fff; }
679 .meowapps-wpai-promo-btn-secondary { background: #7c3aed; color: #fff; }
680 .meowapps-wpai-promo-btn-secondary:hover { background: #6527c9; color: #fff; }
681 .meowapps-wpai-promo-btn-dismiss {
682 margin-left: auto;
683 background: transparent; color: #6b7280;
684 font-weight: 500; padding: 7px 10px;
685 }
686 .meowapps-wpai-promo-btn-dismiss:hover { color: #1e1e1e; background: rgba(0,0,0,0.04); }
687 </style>
688 <script>
689 (function () {
690 var D = <?php echo wp_json_encode( $payload ); ?>;
691 try { if (localStorage.getItem('meowapps-wpai-promo-dismissed') === '1') return; } catch (e) {}
692
693 function build() {
694 var host = document.createElement('div');
695 host.className = 'meowapps-wpai-promo';
696 host.setAttribute('role', 'status');
697 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>';
698 host.innerHTML = [
699 '<span class="meowapps-wpai-promo-icon">', iconSvg, '</span>',
700 '<div class="meowapps-wpai-promo-body">',
701 '<div class="meowapps-wpai-promo-text">',
702 '<strong></strong> <span class="meowapps-wpai-promo-sub"></span>',
703 '</div>',
704 '<div class="meowapps-wpai-promo-actions"></div>',
705 '</div>'
706 ].join('');
707 // Title carries an anchor on "AI Engine" → wp.org plugin page.
708 var strong = host.querySelector('strong');
709 strong.appendChild(document.createTextNode(D.titleBefore));
710 var tLink = document.createElement('a');
711 tLink.href = D.wporgUrl;
712 tLink.target = '_blank';
713 tLink.rel = 'noopener noreferrer';
714 tLink.className = 'meowapps-wpai-promo-titlelink';
715 tLink.textContent = D.titleLink;
716 strong.appendChild(tLink);
717 strong.appendChild(document.createTextNode(D.titleAfter));
718 host.querySelector('.meowapps-wpai-promo-sub').textContent = D.sub;
719 var actions = host.querySelector('.meowapps-wpai-promo-actions');
720
721 function link(label, cls, href, newTab) {
722 var a = document.createElement('a');
723 a.className = 'meowapps-wpai-promo-btn ' + cls;
724 a.textContent = label;
725 a.href = href;
726 if (newTab) { a.target = '_blank'; a.rel = 'noopener noreferrer'; }
727 actions.appendChild(a);
728 return a;
729 }
730
731 // Install → WordPress's plugin-install search page, pre-filtered.
732 // User lands on a familiar screen with AI Engine as the top hit and
733 // can install with the native WordPress UX.
734 link(D.install, 'meowapps-wpai-promo-btn-primary', D.installUrl, false);
735 link(D.learn, 'meowapps-wpai-promo-btn-secondary', D.learnUrl, true);
736
737 var d = document.createElement('button');
738 d.type = 'button';
739 d.className = 'meowapps-wpai-promo-btn meowapps-wpai-promo-btn-dismiss';
740 d.textContent = D.dismiss;
741 d.addEventListener('click', function () {
742 try { localStorage.setItem('meowapps-wpai-promo-dismissed', '1'); } catch (e) {}
743 if (host.parentNode) host.parentNode.removeChild(host);
744 });
745 actions.appendChild(d);
746 return host;
747 }
748
749 function ensure() {
750 if (document.querySelector('.meowapps-wpai-promo')) return;
751 var page = document.querySelector('.connectors-page');
752 if (page) { page.insertBefore(build(), page.firstChild); return; }
753 var header = document.querySelector('.boot-layout__stage header');
754 if (header && header.parentNode) {
755 header.parentNode.insertBefore(build(), header.nextSibling);
756 }
757 }
758
759 var tries = 0;
760 var iv = setInterval(function () {
761 ensure();
762 if (document.querySelector('.meowapps-wpai-promo') || ++tries > 40) clearInterval(iv);
763 }, 120);
764
765 var app = document.getElementById('options-connectors-wp-admin-app')
766 || document.getElementById('options-connectors-app');
767 if (app && 'MutationObserver' in window) {
768 new MutationObserver(ensure).observe(app, { childList: true, subtree: true });
769 }
770 })();
771 </script>
772 <?php
773 }
774 }
775 }
776