PluginProbe
Meow Gallery / 5.5.5
Meow Gallery v5.5.5
5.5.5 5.5.4 5.5.3 5.5.2 5.5.1 5.5.0 5.4.9 5.4.8 5.4.7 4.1.5 4.1.6 4.1.7 4.1.8 4.1.9 4.2.0 4.2.1 4.2.2 4.2.3 4.2.4 4.2.5 4.2.6 4.2.7 4.2.8 4.2.9 4.3.0 All 157 releases
← All changes | common/admin.php +294 -17 5.4.75.5.5 View file →
@@ -3,10 +3,10 @@
3 3 if ( !class_exists( 'MeowKit_MGL_Admin' ) ) {
4 4
5 5 class MeowKit_MGL_Admin {
6 6 public static $loaded = false;
7 - public static $version = '4.0';
8 - public static $admin_version = '4.0';
7 + public static $version = '5.0';
8 + public static $admin_version = '5.0';
9 9 public static $network_license_modal_added = false;
10 10 public static $network_license_plugins = [];
11 11
12 12 /**
@@ -157,8 +157,13 @@
157 157 if ( $page === 'meowapps-main-menu' ) {
158 158 add_filter( 'admin_footer_text', [ $this, 'admin_footer_text' ], 100000, 1 );
159 159 }
160 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 +
161 166 MeowKit_MGL_Admin::$loaded = true;
162 167 }
163 168
164 169 // PER-INSTANCE SETUP: Run for each plugin that uses this library
@@ -197,8 +202,45 @@
197 202 if ( !$isActive ) {
198 203 return $links;
199 204 }
200 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 + }
201 243 if ( strpos( $pathName, $thisPathName ) !== false ) {
202 244 // In network admin, handle differently (no settings page available)
203 245 if ( is_network_admin() ) {
204 246 if ( $this->isPro && !$this->is_registered() ) {
@@ -239,9 +281,9 @@
239 281 sprintf( __( '<a href="admin.php?page=%s_settings">Settings</a>', $this->domain ), $this->prefix ),
240 282 'license' =>
241 283 $this->is_registered() ?
242 284 ( '<span style="color: #a75bd6;">' . __( 'Pro Version', $this->domain ) . '</span>' ) :
243 - ( $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>' ) ),
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>' ) ),
244 286 ];
245 287 }
246 288 $links = array_merge( $new_links, $links );
247 289 }
@@ -267,9 +309,11 @@
267 309 <button type="button" id="meowapps-license-submit" class="button button-primary">Validate & Register</button>
268 310 </div>
269 311 </div>
270 312 </div>
271 - <script>
313 + <?php
314 + ob_start();
315 + ?>
272 316 (function() {
273 317 var modal = document.getElementById('meowapps-network-license-modal');
274 318 var input = document.getElementById('meowapps-license-key-input');
275 319 var message = document.getElementById('meowapps-license-message');
@@ -379,10 +423,10 @@
379 423 submitBtn.textContent = 'Validate & Register';
380 424 });
381 425 });
382 426 })();
383 - </script>
384 427 <?php
428 + wp_print_inline_script_tag( ob_get_clean() );
385 429 }
386 430
387 431 public function request_verify_ssl() {
388 432 return get_option( 'force_sslverify', false );
@@ -400,9 +444,12 @@
400 444 return $file;
401 445 }
402 446
403 447 public function admin_notices_licensed_free() {
404 - if ( isset( $_POST[$this->prefix . '_reset_sub'] ) ) {
448 + // Verify the nonce before removing the license from a POST request (CSRF protection).
449 + if ( isset( $_POST[$this->prefix . '_reset_sub'] )
450 + && isset( $_POST[ $this->prefix . '_reset_sub_nonce' ] )
451 + && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ $this->prefix . '_reset_sub_nonce' ] ) ), $this->prefix . '_reset_sub' ) ) {
405 452 delete_option( $this->prefix . '_pro_serial' );
406 453 delete_option( $this->prefix . '_license' );
407 454 return;
408 455 }
@@ -412,9 +459,9 @@
412 459 $this->nice_name_from_file( $this->mainfile )
413 460 );
414 461 $html .= '<p>
415 462 <form method="post" action="">
416 - <input type="hidden" name="' . $this->prefix . '_reset_sub" value="true">
463 + <input type="hidden" name="' . $this->prefix . '_reset_sub" value="true"><input type="hidden" name="' . $this->prefix . '_reset_sub_nonce" value="' . esc_attr( wp_create_nonce( $this->prefix . '_reset_sub' ) ) . '">
417 464 <input type="submit" name="submit" id="submit" class="button" value="'
418 465 . __( 'Remove the license', $this->domain ) . '">
419 466 </form>
420 467 </p>';
@@ -424,19 +471,23 @@
424 471
425 472 public function admin_menu_start() {
426 473 // Hide the admin if user doesn't like Meow much
427 474 if ( get_option( 'meowapps_hide_meowapps', false ) ) {
428 - register_setting( 'general', 'meowapps_hide_meowapps' );
475 + register_setting( 'general', 'meowapps_hide_meowapps', [ 'type' => 'boolean', 'sanitize_callback' => 'rest_sanitize_boolean' ] );
429 476 add_settings_field( 'meowapps_hide_ads', 'Meow Apps Menu', [ $this, 'meowapps_hide_dashboard_callback' ], 'general' );
430 477 return;
431 478 }
432 479
433 - // Create standard menu if it does not already exist
480 + // Create standard menu if it does not already exist.
481 + // The cat logo is injected as an <img> inside the menu title (rather than passed as
482 + // the $icon_url argument) so the original SVG fills are preserved — passing it via
483 + // $icon_url makes WordPress add the .svg class and the admin color scheme strips
484 + // the colors to a single fill.
434 485 global $submenu;
435 486 if ( !isset( $submenu[ 'meowapps-main-menu' ] ) ) {
436 487 add_menu_page(
437 488 'Meow Apps',
438 - '<img alt="Meow Apps" style="width: 21px; margin-left: -28px; position: absolute; margin-top: 2px;" src="' . MeowKit_MGL_Admin::$logo . '" />Meow Apps',
489 + '<img alt="Meow Apps" class="meowapps-menu-icon" src="' . MeowKit_MGL_Admin::$logo . '" />Meow Apps',
439 490 'manage_options',
440 491 'meowapps-main-menu',
441 492 [ $this, 'admin_meow_apps' ],
442 493 '',
@@ -451,15 +502,46 @@
451 502 [ $this, 'admin_meow_apps' ]
452 503 );
453 504 }
454 505
455 - // Add CSS to hide the default icon
456 - add_action( 'admin_head', function () {
457 - echo '<style>
458 - #toplevel_page_meowapps-main-menu .wp-menu-image {
459 - display: none;
460 - }
461 - </style>';
506 + // Position the cat icon so it sits in the standard icon column in both the
507 + // expanded and the collapsed (folded) sidebar.
508 + //
509 + // The image lives inside the .wp-menu-name title (where the original code
510 + // put it) so the <img> renders with its native SVG fills. When the sidebar
511 + // is collapsed, WP hides .wp-menu-name (and everything inside it), so a
512 + // small JS snippet below clones the same <img> into the .wp-menu-image
513 + // slot — which WP keeps visible in both states. We use a real <img>
514 + // (not a background-image) on purpose: at small sizes WP's admin renders
515 + // background-image SVGs noticeably lighter than the equivalent <img>,
516 + // and we want the colored cat in both states.
517 + //
518 + // The !important rules also override an older "display: none" style that
519 + // previous-generation Meow Apps common copies inject for .wp-menu-image;
520 + // if any of them load first, ours still wins.
521 + // Enqueue the menu-icon CSS/JS through the assets API instead of echoing inline tags.
522 + add_action( 'admin_enqueue_scripts', function () {
523 + $css = '
524 + #toplevel_page_meowapps-main-menu .meowapps-menu-icon { width: 20px; height: auto; position: absolute; margin-left: -28px; margin-top: 3px; }
525 + #toplevel_page_meowapps-main-menu .wp-menu-image { display: block !important; position: relative; }
526 + #toplevel_page_meowapps-main-menu .wp-menu-image::before { display: none !important; }
527 + #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded { display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, calc(-50% - 3px)); width: 20px; height: auto; }
528 + body.folded #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded { display: block; }
529 + body.folded #toplevel_page_meowapps-main-menu .meowapps-menu-icon { display: none; }
530 + @media only screen and (max-width: 960px) {
531 + body.auto-fold #toplevel_page_meowapps-main-menu .wp-menu-image .meowapps-menu-icon-folded { display: block; }
532 + body.auto-fold #toplevel_page_meowapps-main-menu .meowapps-menu-icon { display: none; }
533 + }';
534 + wp_add_inline_style( 'admin-menu', $css );
535 +
536 + // Clone the in-title cat icon into the .wp-menu-image slot (CSS shows it only when folded).
537 + $js = 'document.addEventListener("DOMContentLoaded", function () {'
538 + . 'var li = document.getElementById("toplevel_page_meowapps-main-menu"); if ( !li ) { return; }'
539 + . 'var src = li.querySelector(".meowapps-menu-icon"); var slot = li.querySelector(".wp-menu-image");'
540 + . 'if ( !src || !slot || slot.querySelector(".meowapps-menu-icon-folded") ) { return; }'
541 + . 'var clone = src.cloneNode(); clone.className = "meowapps-menu-icon-folded"; clone.removeAttribute("style"); slot.appendChild(clone);'
542 + . '});';
543 + wp_add_inline_script( 'common', $js );
462 544 } );
463 545 }
464 546
465 547 public function meowapps_hide_dashboard_callback() {
@@ -503,7 +585,202 @@
503 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 ),
504 586 MeowKit_MGL_Admin::$version,
505 587 __FILE__
506 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() );
507 784 }
508 785 }
509 786 }