Changeset 3732385
- Timestamp:
- 10/07/2026 10:44:52 AM (3 weeks ago)
- Location:
- jcodex-ai-product-generator-for-woocommerce
- Files:
-
- 67 added
- 15 edited
-
tags/1.2.6 (added)
-
tags/1.2.6/ai-product-generator.php (added)
-
tags/1.2.6/assets (added)
-
tags/1.2.6/assets/css (added)
-
tags/1.2.6/assets/css/admin.css (added)
-
tags/1.2.6/assets/css/brand-voice.css (added)
-
tags/1.2.6/assets/css/chrome.css (added)
-
tags/1.2.6/assets/css/content-scanner.css (added)
-
tags/1.2.6/assets/css/dashboard.css (added)
-
tags/1.2.6/assets/css/index.php (added)
-
tags/1.2.6/assets/css/optimizer.css (added)
-
tags/1.2.6/assets/fonts (added)
-
tags/1.2.6/assets/fonts/OFL.txt (added)
-
tags/1.2.6/assets/fonts/dm-sans-latin-400-normal.woff2 (added)
-
tags/1.2.6/assets/fonts/dm-sans-latin-500-normal.woff2 (added)
-
tags/1.2.6/assets/fonts/dm-sans-latin-600-normal.woff2 (added)
-
tags/1.2.6/assets/fonts/dm-sans-latin-700-normal.woff2 (added)
-
tags/1.2.6/assets/fonts/dm-sans-latin-ext-400-normal.woff2 (added)
-
tags/1.2.6/assets/fonts/dm-sans-latin-ext-700-normal.woff2 (added)
-
tags/1.2.6/assets/fonts/index.php (added)
-
tags/1.2.6/assets/index.php (added)
-
tags/1.2.6/assets/js (added)
-
tags/1.2.6/assets/js/admin.js (added)
-
tags/1.2.6/assets/js/brand-voice.js (added)
-
tags/1.2.6/assets/js/content-scanner.js (added)
-
tags/1.2.6/assets/js/index.php (added)
-
tags/1.2.6/assets/js/optimizer.js (added)
-
tags/1.2.6/includes (added)
-
tags/1.2.6/includes/class-admin-image.php (added)
-
tags/1.2.6/includes/class-admin-menu.php (added)
-
tags/1.2.6/includes/class-admin.php (added)
-
tags/1.2.6/includes/class-api.php (added)
-
tags/1.2.6/includes/class-brand-voice.php (added)
-
tags/1.2.6/includes/class-content-scanner.php (added)
-
tags/1.2.6/includes/class-language.php (added)
-
tags/1.2.6/includes/class-limits.php (added)
-
tags/1.2.6/includes/class-optimizer.php (added)
-
tags/1.2.6/includes/class-product-health.php (added)
-
tags/1.2.6/includes/class-remote.php (added)
-
tags/1.2.6/includes/class-scanner-admin.php (added)
-
tags/1.2.6/includes/class-seo-integration.php (added)
-
tags/1.2.6/includes/class-seo-output.php (added)
-
tags/1.2.6/includes/index.php (added)
-
tags/1.2.6/languages (added)
-
tags/1.2.6/languages/index.php (added)
-
tags/1.2.6/languages/jcodex-ai-product-generator-for-woocommerce.pot (added)
-
tags/1.2.6/readme.txt (added)
-
tags/1.2.6/uninstall.php (added)
-
trunk/ai-product-generator.php (modified) (9 diffs)
-
trunk/assets/css/admin.css (modified) (6 diffs)
-
trunk/assets/css/chrome.css (added)
-
trunk/assets/css/dashboard.css (modified) (1 diff)
-
trunk/assets/css/index.php (added)
-
trunk/assets/css/optimizer.css (added)
-
trunk/assets/fonts (added)
-
trunk/assets/fonts/OFL.txt (added)
-
trunk/assets/fonts/dm-sans-latin-400-normal.woff2 (added)
-
trunk/assets/fonts/dm-sans-latin-500-normal.woff2 (added)
-
trunk/assets/fonts/dm-sans-latin-600-normal.woff2 (added)
-
trunk/assets/fonts/dm-sans-latin-700-normal.woff2 (added)
-
trunk/assets/fonts/dm-sans-latin-ext-400-normal.woff2 (added)
-
trunk/assets/fonts/dm-sans-latin-ext-700-normal.woff2 (added)
-
trunk/assets/fonts/index.php (added)
-
trunk/assets/index.php (added)
-
trunk/assets/js/admin.js (modified) (13 diffs)
-
trunk/assets/js/index.php (added)
-
trunk/assets/js/optimizer.js (added)
-
trunk/includes/class-admin-image.php (modified) (1 diff)
-
trunk/includes/class-admin-menu.php (modified) (6 diffs)
-
trunk/includes/class-admin.php (modified) (12 diffs)
-
trunk/includes/class-api.php (modified) (25 diffs)
-
trunk/includes/class-brand-voice.php (modified) (2 diffs)
-
trunk/includes/class-content-scanner.php (modified) (2 diffs)
-
trunk/includes/class-language.php (modified) (1 diff)
-
trunk/includes/class-limits.php (modified) (2 diffs)
-
trunk/includes/class-optimizer.php (added)
-
trunk/includes/class-product-health.php (modified) (10 diffs)
-
trunk/includes/class-remote.php (modified) (20 diffs)
-
trunk/includes/index.php (added)
-
trunk/languages/index.php (added)
-
trunk/readme.txt (modified) (1 diff)
-
trunk/uninstall.php (added)
Legend:
- Unmodified
- Added
- Removed
-
jcodex-ai-product-generator-for-woocommerce/trunk/ai-product-generator.php
r3724671 r3732385 1 1 <?php 2 2 /** 3 * Plugin Name: All -in-OneAI Product Manager for WooCommerce3 * Plugin Name: All in One - AI Product Manager for WooCommerce 4 4 * Description: Generate product descriptions, short descriptions, features, benefits, tags, SEO titles and more using AI directly in WooCommerce. 5 5 * Author: Jcodex 6 * Version: 1.2.5 6 * Version: 1.2.6 7 * Requires at least: 5.8 8 * Requires PHP: 7.4 7 9 * License: GPLv2 or later 8 10 * License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 19 21 * This program is free software: you can redistribute it and/or modify 20 22 * it under the terms of the GNU General Public License as published by 21 * the Free Software Foundation, either version 3of the License, or23 * the Free Software Foundation, either version 2 of the License, or 22 24 * (at your option) any later version. 23 25 */ … … 27 29 } 28 30 29 define( 'JCODEX_APGWCO_VERSION', '1.2.5' ); 31 /* 32 * Free and Pro share class names and option keys. They cannot run together. 33 * If Pro already loaded (it defines these constants first), leave quietly 34 * and switch this copy off so it cannot redefine them. 35 */ 36 if ( defined( 'JCODEX_APGWCO_PATH' ) || class_exists( 'Jcodex_Apgwco_Plugin', false ) ) { 37 if ( ! function_exists( 'deactivate_plugins' ) ) { 38 require_once ABSPATH . 'wp-admin/includes/plugin.php'; 39 } 40 deactivate_plugins( plugin_basename( __FILE__ ), true ); 41 return; 42 } 43 44 define( 'JCODEX_APGWCO_VERSION', '1.2.6' ); 30 45 define( 'JCODEX_APGWCO_PATH', plugin_dir_path( __FILE__ ) ); 31 46 define( 'JCODEX_APGWCO_URL', plugin_dir_url( __FILE__ ) ); … … 33 48 define( 'JCODEX_APGWCO_PRO_URL', 'https://jcodex.com/plugins/jcodex-ai-product-generator-for-woocommerce/' ); 34 49 50 if ( ! function_exists( 'jcodex_apgwco_activate' ) ) { 51 function jcodex_apgwco_activate() { 52 update_option( 'jcodex_apgwco_activation_redirect', 1, false ); 53 } 54 } 55 register_activation_hook( __FILE__, 'jcodex_apgwco_activate' ); 56 57 if ( ! function_exists( 'jcodex_apgwco_maybe_activation_redirect' ) ) { 58 function jcodex_apgwco_maybe_activation_redirect() { 59 if ( ! get_option( 'jcodex_apgwco_activation_redirect' ) ) { 60 return; 61 } 62 if ( ! current_user_can( 'activate_plugins' ) ) { 63 return; 64 } 65 if ( wp_doing_ajax() || ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) ) { 66 return; 67 } 68 if ( is_network_admin() ) { 69 return; 70 } 71 if ( defined( 'WP_CLI' ) && WP_CLI ) { 72 return; 73 } 74 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- activation flag only. 75 if ( isset( $_GET['activate-multi'] ) ) { 76 return; 77 } 78 delete_option( 'jcodex_apgwco_activation_redirect' ); 79 wp_safe_redirect( admin_url( 'admin.php?page=jcodex-apgwco' ) ); 80 exit; 81 } 82 } 83 add_action( 'admin_init', 'jcodex_apgwco_maybe_activation_redirect' ); 84 35 85 /** AI product image generation — enabled in free version (10 uses). */ 36 86 define( 'JCODEX_APGWCO_IMAGE_ENABLED', true ); … … 40 90 $default = defined( 'JCODEX_APGWCO_IMAGE_ENABLED' ) && JCODEX_APGWCO_IMAGE_ENABLED; 41 91 return (bool) apply_filters( 'jcodex_apgwco_image_enabled', $default ); 92 } 93 } 94 95 if ( ! function_exists( 'jcodex_apgwco_enqueue_admin_chrome' ) ) { 96 /** 97 * Load DM Sans and the shared type/color rules on plugin screens only. 98 * 99 * @param string $hook Current admin page hook. 100 */ 101 function jcodex_apgwco_enqueue_admin_chrome( $hook ) { 102 $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 103 $is_plugin_page = ( 0 === strpos( $page, 'jcodex-apgwco' ) ); 104 $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; 105 $is_product = $screen && ! empty( $screen->post_type ) && 'product' === $screen->post_type && in_array( $hook, array( 'post.php', 'post-new.php' ), true ); 106 107 if ( ! $is_plugin_page && ! $is_product ) { 108 return; 109 } 110 111 wp_enqueue_style( 112 'jcodex-apgwco-chrome', 113 JCODEX_APGWCO_URL . 'assets/css/chrome.css', 114 array(), 115 JCODEX_APGWCO_VERSION 116 ); 117 } 118 } 119 120 if ( ! function_exists( 'jcodex_apgwco_admin_body_class' ) ) { 121 /** 122 * Mark plugin admin pages so chrome.css can scope the type scale. 123 * 124 * @param string $classes Existing body classes. 125 * @return string 126 */ 127 function jcodex_apgwco_admin_body_class( $classes ) { 128 $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended 129 if ( 0 === strpos( $page, 'jcodex-apgwco' ) ) { 130 $classes .= ' jcodex-apgwco-screen'; 131 } 132 return $classes; 42 133 } 43 134 } … … 54 145 require_once JCODEX_APGWCO_PATH . 'includes/class-language.php'; 55 146 require_once JCODEX_APGWCO_PATH . 'includes/class-brand-voice.php'; 147 require_once JCODEX_APGWCO_PATH . 'includes/class-optimizer.php'; 56 148 require_once JCODEX_APGWCO_PATH . 'includes/class-api.php'; 57 149 … … 70 162 71 163 private function __construct() { 164 add_action( 'init', array( $this, 'load_textdomain' ), 0 ); 72 165 add_action( 'plugins_loaded', array( $this, 'init' ) ); 73 166 } … … 79 172 } 80 173 81 $this->load_textdomain(); 174 add_action( 'admin_enqueue_scripts', 'jcodex_apgwco_enqueue_admin_chrome', 99 ); 175 add_filter( 'admin_body_class', 'jcodex_apgwco_admin_body_class' ); 82 176 83 177 new Jcodex_Apgwco_Admin(); … … 87 181 new Jcodex_Apgwco_Language(); 88 182 new Jcodex_Apgwco_Brand_Voice(); 183 new Jcodex_Apgwco_Optimizer(); 89 184 new Jcodex_Apgwco_API(); 90 185 new Jcodex_Apgwco_Seo_Output(); -
jcodex-ai-product-generator-for-woocommerce/trunk/assets/css/admin.css
r3724671 r3732385 36 36 height: 36px; 37 37 border-radius: 10px; 38 background: rgb a(255,255,255,0.2);38 background: rgb(255, 255, 255); 39 39 color: #fff; 40 40 font-size: 11px; 41 padding: 6px; 41 42 font-weight: 800; 42 43 letter-spacing: 0.05em; 43 44 flex-shrink: 0; 45 } 46 47 .jcodex-apgwco-badge svg { 48 width: 26px; 49 height: 26px; 44 50 } 45 51 … … 83 89 .jcodex-apgwco-label { 84 90 display: block; 85 font-size: 1 2px;91 font-size: 14px; 86 92 font-weight: 700; 87 93 color: #374151; … … 297 303 298 304 .jcodex-apgwco-btn:disabled, 299 .jcodex-apgwco-btn.primary:disabled { 305 .jcodex-apgwco-btn.primary:disabled, 306 a.jcodex-apgwco-btn.jcodex-apgwco-btn-premium { 300 307 opacity: 0.45; 301 308 cursor: not-allowed; … … 304 311 border: 1px solid #e5e7eb !important; 305 312 box-shadow: none !important; 313 pointer-events: auto; 314 } 315 316 a.jcodex-apgwco-btn.jcodex-apgwco-btn-premium:hover { 317 background: #e5e7eb !important; 318 color: #9ca3af !important; 306 319 } 307 320 … … 310 323 font-size: 13px; 311 324 min-height: 20px; 312 margin -bottom:4px;325 margin: 10px 0 4px; 313 326 } 314 327 … … 428 441 .jcodex-apgwco-health-bar { height: 7px; overflow: hidden; border-radius: 99px; background: #dbeafe; } 429 442 .jcodex-apgwco-health-bar i { display: block; height: 100%; border-radius: inherit; background: linear-gradient(90deg, #2563eb, #4f46e5); } 443 .jcodex-apgwco-health-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; } 444 .jcodex-apgwco-health-actions .jcodex-apgwco-btn { text-decoration: none; } 445 .jcodex-apgwco-health-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px 24px; margin-top: 14px; } 446 .jcodex-apgwco-health-group h4 { margin: 0 0 8px; color: #1e3a8a; font-size: 12px; font-weight: 700; letter-spacing: .02em; text-transform: uppercase; } 447 .jcodex-apgwco-health-list { margin: 0; padding: 0; list-style: none; } 448 .jcodex-apgwco-health-list li { display: flex; align-items: flex-start; gap: 8px; margin: 0 0 6px; color: #334155; font-size: 13px; line-height: 1.35; } 449 .jcodex-apgwco-health-mark { width: 1em; flex-shrink: 0; font-weight: 700; line-height: 1.35; } 450 .jcodex-apgwco-health-list .is-ok { color: #166534; } 451 .jcodex-apgwco-health-list .is-ok .jcodex-apgwco-health-mark::before { content: "\2713"; color: #16a34a; } 452 .jcodex-apgwco-health-list .is-weak { color: #92400e; } 453 .jcodex-apgwco-health-list .is-weak .jcodex-apgwco-health-mark::before { content: "!"; color: #d97706; } 454 .jcodex-apgwco-health-list .is-missing { color: #991b1b; } 455 .jcodex-apgwco-health-list .is-missing .jcodex-apgwco-health-mark::before { content: "\2715"; color: #dc2626; } 456 .jcodex-apgwco-health-gaps { margin: 12px 0 0; padding: 0; list-style: none; display: flex; flex-wrap: wrap; gap: 6px; } 457 .jcodex-apgwco-health-gaps li { display: inline-flex; align-items: center; gap: 6px; margin: 0; padding: 4px 10px; border-radius: 999px; background: #fff; border: 1px solid #e2e8f0; color: #334155; font-size: 12px; } 458 .jcodex-apgwco-health-dot { width: 7px; height: 7px; border-radius: 50%; background: #94a3b8; flex-shrink: 0; } 459 .jcodex-apgwco-health-gaps .is-missing .jcodex-apgwco-health-dot { background: #dc2626; } 460 .jcodex-apgwco-health-gaps .is-weak .jcodex-apgwco-health-dot { background: #d97706; } 461 .jcodex-apgwco-health-gaps .is-manual { color: #64748b; border-style: dashed; } 462 .jcodex-apgwco-health-hint { margin: 8px 0 0; color: #64748b; font-size: 12px; } 463 .jcodex-apgwco-health-tips { margin: 10px 0 0; padding-left: 16px; color: #64748b; font-size: 12px; } 430 464 .jcodex-apgwco-health-issues { margin: 8px 0 0; padding-left: 18px; color: #475569; font-size: 12px; } 431 465 .jcodex-apgwco-health-issues li { margin: 7px 0; } 466 467 @media (max-width: 782px) { 468 .jcodex-apgwco-health-grid { grid-template-columns: 1fr; } 469 } 432 470 433 471 @media (max-width: 600px) { -
jcodex-ai-product-generator-for-woocommerce/trunk/assets/css/dashboard.css
r3724671 r3732385 7 7 .jcodex-apgwco-dashboard-card .jcodex-apgwco-dashboard-meta { font-size: 12px; color: #64748b; } 8 8 .jcodex-apgwco-dashboard-badge { padding: 2px 8px; border-radius: 999px; background: #dcfce7; color: #15803d; font-size: 11px; font-weight: 600; } 9 .jcodex-apgwco-dashboard-badge-pro { background: #e0e7ff; color: #1e40af; } 9 10 .jcodex-apgwco-dashboard-pro { border-color: #bbf7d0; background: #f0fdf4; } 11 .jcodex-apgwco-dashboard-card-locked { 12 border-style: dashed; 13 background: #f8fafc; 14 } 15 .jcodex-apgwco-dashboard-card-locked h2, 16 .jcodex-apgwco-dashboard-card-locked p { 17 opacity: 0.72; 18 } 19 .jcodex-apgwco-dashboard-card-locked .button-primary { 20 background: linear-gradient(135deg, #1e40af 0%, #4f46e5 100%); 21 border-color: transparent; 22 color: #fff; 23 } 10 24 11 25 .jcodex-apgwco-language-card { max-width: 640px; margin-top: 18px; } -
jcodex-ai-product-generator-for-woocommerce/trunk/assets/js/admin.js
r3724671 r3732385 22 22 23 23 function setLoading( btn ) { 24 usageSyncStale = true; 24 25 btn.data( 'original-text', btn.html() ); 25 26 btn.html( i18n.generating ); … … 45 46 } 46 47 47 function updateLimitBadge( type, remaining ) { 48 $( '[data-type="' + type + '"]' ).text( remaining ); 48 var usageSyncStale = false; 49 50 function updateLimitBadge( type, remaining, fromSync ) { 51 remaining = parseInt( remaining, 10 ); 52 if ( isNaN( remaining ) ) { 53 return; 54 } 55 if ( fromSync ) { 56 if ( usageSyncStale ) { 57 return; 58 } 59 var current = parseInt( $( '.jcodex-apgwco-limit-badge[data-type="' + type + '"]' ).first().text(), 10 ); 60 if ( ! isNaN( current ) && remaining > current ) { 61 return; 62 } 63 } else { 64 usageSyncStale = true; 65 } 66 var display = remaining; 67 $( '.jcodex-apgwco-limit-badge[data-type="' + type + '"]' ).text( display ); 68 $( '.jcodex-apgwco-limit-badge-inline[data-type="' + type + '"]' ).text( display + ' ' + ( i18n.remaining || 'left' ) ); 69 $( '.jcodex-apgwco-image-remaining-number[data-type="' + type + '"]' ).text( display ); 70 } 71 72 function applyRemaining( type, res ) { 73 if ( ! res || ! res.data || res.data.free_regenerate ) { 74 return; 75 } 76 if ( typeof res.data.remaining === 'undefined' ) { 77 return; 78 } 79 updateLimitBadge( type, res.data.remaining ); 80 } 81 82 if ( ajax.needs_usage_sync ) { 83 $.post( ajaxUrl, { 84 action: 'jcodex_apgwco_sync_usage', 85 nonce: nonce 86 }, function ( res ) { 87 if ( usageSyncStale || ! res || ! res.success || ! res.data || ! res.data.remaining ) { 88 return; 89 } 90 $.each( res.data.remaining, function ( type, value ) { 91 updateLimitBadge( type, value, true ); 92 } ); 93 } ); 49 94 } 50 95 … … 186 231 187 232 showStatus( $status, i18n.descGenerated + bonusNote( res ), false, false ); 188 updateLimitBadge( 'text', res.data.remaining);233 applyRemaining( 'text', res ); 189 234 } else { 190 235 showStatus( $status, res.data.message, true, true ); … … 237 282 238 283 showStatus( $status, i18n.shortDescApplied + bonusNote( res ), false, false ); 239 updateLimitBadge( 'short_desc', res.data.remaining);284 applyRemaining( 'short_desc', res ); 240 285 } else { 241 286 showStatus( $status, res.data.message, true, true ); … … 275 320 $output.show(); 276 321 $status.html( bonusNote( res ).trim() ); 277 updateLimitBadge( 'features', res.data.remaining);322 applyRemaining( 'features', res ); 278 323 } else { 279 324 showStatus( $status, res.data.message, true, true ); … … 314 359 $output.show(); 315 360 $status.html( bonusNote( res ).trim() ); 316 updateLimitBadge( 'benefits', res.data.remaining);361 applyRemaining( 'benefits', res ); 317 362 } else { 318 363 showStatus( $status, res.data.message, true, true ); … … 363 408 $output.show(); 364 409 $status.html( bonusNote( res ).trim() ); 365 updateLimitBadge( 'tags', res.data.remaining);410 applyRemaining( 'tags', res ); 366 411 } else { 367 412 showStatus( $status, res.data.message, true, true ); … … 447 492 $output.show(); 448 493 $status.html( bonusNote( res ).trim() ); 449 updateLimitBadge( 'seo_title', res.data.remaining);494 applyRemaining( 'seo_title', res ); 450 495 } else { 451 496 showStatus( $status, res.data.message, true, true ); … … 685 730 $output.show(); 686 731 $status.html( bonusNote( res ).trim() ); 687 updateLimitBadge( 'meta_desc', res.data.remaining);732 applyRemaining( 'meta_desc', res ); 688 733 } else { 689 734 showStatus( $status, res.data.message, true, true ); … … 779 824 $output.show(); 780 825 $status.html( bonusNote( res ).trim() ); 781 updateLimitBadge( 'name_improve', res.data.remaining);826 applyRemaining( 'name_improve', res ); 782 827 } else { 783 828 showStatus( $status, res.data.message, true, true ); … … 810 855 var healthResults = $( '#jcodex-apgwco-health-results' ); 811 856 812 function healthItem( item ) { 813 return $( '<li />' ).text( item.message || item ); 857 function liveProductTitle() { 858 var $classic = $( '#title' ); 859 if ( $classic.length ) { 860 return $.trim( $classic.val() || '' ); 861 } 862 var $block = $( '.editor-post-title__input' ).first(); 863 if ( $block.length ) { 864 return $.trim( $block.val() || $block.text() || '' ); 865 } 866 return ''; 814 867 } 815 868 816 869 function renderHealth( analysis ) { 817 if ( ! analysis || ! analysis.categories) return;870 if ( ! analysis ) return; 818 871 healthScore.empty().append( document.createTextNode( analysis.overall_score ) ).append( $( '<small />' ).text( '/ 100' ) ); 819 healthSummary.text( analysis.overall_score >= 70 ? i18n.healthGood : i18n.healthNeedsWork ); 872 var groups = [ 873 { key: 'content', label: ( i18n.categoryLabels && i18n.categoryLabels.content ) || 'Content' }, 874 { key: 'seo', label: ( i18n.categoryLabels && i18n.categoryLabels.seo ) || 'SEO' }, 875 { key: 'images', label: ( i18n.categoryLabels && i18n.categoryLabels.images ) || 'Images' }, 876 { key: 'conversion', label: ( i18n.categoryLabels && i18n.categoryLabels.conversion ) || 'Conversion' } 877 ]; 878 var gapCount = 0; 820 879 healthResults.empty(); 821 822 var $categories = $( '<div class="jcodex-apgwco-health-categories" />' ); 823 $.each( analysis.categories, function ( key, category ) { 824 var label = i18n.categoryLabels && i18n.categoryLabels[ key ] ? i18n.categoryLabels[ key ] : key; 825 var $row = $( '<div class="jcodex-apgwco-health-category" />' ); 826 $row.append( $( '<span />' ).text( label ) ); 827 $row.append( $( '<span class="jcodex-apgwco-health-bar" aria-hidden="true" />' ).append( $( '<i />' ).css( 'width', Math.max( 0, Math.min( 100, category.score ) ) + '%' ) ) ); 828 $row.append( $( '<strong />' ).text( category.score + '/100' ) ); 829 $categories.append( $row ); 830 } ); 831 healthResults.append( $categories ); 832 833 if ( analysis.priority_issues && analysis.priority_issues.length ) { 834 healthResults.append( $( '<h4 />' ).text( i18n.priorityIssues ) ); 835 var $issues = $( '<ul class="jcodex-apgwco-health-issues" />' ); 836 $.each( analysis.priority_issues, function ( _, item ) { $issues.append( healthItem( item ) ); } ); 837 healthResults.append( $issues ); 838 } 839 if ( analysis.quick_wins && analysis.quick_wins.length ) { 840 healthResults.append( $( '<h4 />' ).text( i18n.quickWins ) ); 841 var $wins = $( '<ul class="jcodex-apgwco-health-issues" />' ); 842 $.each( analysis.quick_wins, function ( _, item ) { $wins.append( healthItem( item ) ); } ); 843 healthResults.append( $wins ); 844 } 880 var $grid = $( '<div class="jcodex-apgwco-health-grid" />' ); 881 $.each( groups, function ( _, group ) { 882 var items = ( analysis.checklist && analysis.checklist[ group.key ] ) || []; 883 if ( ! items.length ) { 884 return; 885 } 886 var $section = $( '<section class="jcodex-apgwco-health-group" />' ); 887 $section.append( $( '<h4 />' ).text( group.label ) ); 888 var $list = $( '<ul class="jcodex-apgwco-health-list" />' ); 889 $.each( items, function ( __, item ) { 890 if ( ! item || ! item.label ) { 891 return; 892 } 893 var status = item.status || 'missing'; 894 if ( status !== 'ok' ) { 895 gapCount++; 896 } 897 $list.append( 898 $( '<li />' ) 899 .addClass( 'is-' + status ) 900 .append( $( '<span class="jcodex-apgwco-health-mark" aria-hidden="true" />' ) ) 901 .append( $( '<span />' ).text( item.label ) ) 902 ); 903 } ); 904 $section.append( $list ); 905 $grid.append( $section ); 906 } ); 907 healthResults.append( $grid ); 908 healthSummary.text( 909 gapCount 910 ? ( i18n.toFix || '%d to fix' ).replace( '%d', String( gapCount ) ) 911 : ( i18n.allClear || i18n.healthGood ) 912 ); 845 913 healthResults.prop( 'hidden', false ); 846 914 } 847 915 916 function setHealthButtonLabel( hasScore ) { 917 healthButton.text( hasScore ? ( i18n.healthRecheck || 'Re-check' ) : ( i18n.healthCheck || 'Check' ) ); 918 } 919 848 920 healthButton.on( 'click', function () { 849 var original= healthButton.text();921 var previous = healthButton.text(); 850 922 healthButton.prop( 'disabled', true ).text( i18n.analyzing ); 851 923 healthStatus.empty(); 852 $.post( ajaxUrl, { action: 'jcodex_apgwco_analyze_product_health', nonce: nonce, post_id: ajax.post_id }, function ( res ) {853 healthButton.prop( 'disabled', false ) .text( original );924 $.post( ajaxUrl, { action: 'jcodex_apgwco_analyze_product_health', nonce: nonce, post_id: ajax.post_id, title: liveProductTitle() }, function ( res ) { 925 healthButton.prop( 'disabled', false ); 854 926 if ( res.success ) { 855 927 renderHealth( res.data.analysis ); 856 showStatus( healthStatus, res.data.analysis.ai_warning || i18n.analysisComplete, !! res.data.ai_warning, false ); 857 updateLimitBadge( 'text', res.data.remaining ); 928 setHealthButtonLabel( true ); 929 if ( res.data.analysis.ai_warning ) { 930 showStatus( healthStatus, res.data.analysis.ai_warning, true, false ); 931 } 932 applyRemaining( 'text', res ); 858 933 } else { 934 healthButton.text( previous ); 859 935 showStatus( healthStatus, res.data && res.data.message ? res.data.message : i18n.analysisFailed, true, false ); 860 936 } 861 937 } ).fail( function () { 862 healthButton.prop( 'disabled', false ).text( original);938 healthButton.prop( 'disabled', false ).text( previous ); 863 939 showStatus( healthStatus, i18n.analysisFailed, true, false ); 864 940 } ); 865 941 } ); 866 942 867 if ( ajax.health && ajax.health.cached ) renderHealth( ajax.health.cached );943 // Health stays idle until the merchant clicks Check. 868 944 869 945 // --------------------------------------------------------------- … … 946 1022 // Preset button click handler 947 1023 $( document ).on( 'click', '.jcodex-apgwco-preset-btn', function () { 948 $( '.jcodex-apgwco-preset-btn' ). css( { 'background': '#fff', 'color': '#333', 'border-color': '#ddd' });949 $( this ). css( { 'background': '#7c3aed', 'color': '#fff', 'border-color': '#7c3aed' });1024 $( '.jcodex-apgwco-preset-btn' ).removeClass( 'is-active' ); 1025 $( this ).addClass( 'is-active' ); 950 1026 $( '#jcodex-apgwco-style-preset' ).val( $( this ).data( 'preset' ) ); 951 1027 } ); … … 1032 1108 updateProductFeaturedImage( res.data.attach_id, res.data.thumbnail_html ); 1033 1109 showStatus( imageStatus, imageI18n.imageAdded + bonusNote( res ), false, false ); 1034 updateLimitBadge( 'image', res.data.remaining);1110 applyRemaining( 'image', res ); 1035 1111 } else { 1036 1112 showStatus( imageStatus, res.data.message, true, true ); -
jcodex-ai-product-generator-for-woocommerce/trunk/includes/class-admin-image.php
r3724671 r3732385 87 87 <?php esc_html_e( 'Quick Style', 'jcodex-ai-product-generator-for-woocommerce' ); ?> 88 88 </label> 89 <div class="jcodex-apgwco-style-presets" style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:8px;">90 <button type="button" class="jcodex-apgwco-preset-btn " data-preset="ecommerce" style="padding:4px 10px;border:2px solid #7c3aed;background:#7c3aed;color:#fff;border-radius:4px;cursor:pointer;font-size:11px;">🛒 E-commerce</button>91 <button type="button" class="jcodex-apgwco-preset-btn" data-preset="lifestyle" style="padding:4px 10px;border:2px solid #ddd;background:#fff;color:#333;border-radius:4px;cursor:pointer;font-size:11px;">✨ Lifestyle</button>92 <button type="button" class="jcodex-apgwco-preset-btn" data-preset="luxury" style="padding:4px 10px;border:2px solid #ddd;background:#fff;color:#333;border-radius:4px;cursor:pointer;font-size:11px;">💎 Luxury</button>93 <button type="button" class="jcodex-apgwco-preset-btn" data-preset="minimal" style="padding:4px 10px;border:2px solid #ddd;background:#fff;color:#333;border-radius:4px;cursor:pointer;font-size:11px;">⬜ Minimal</button>89 <div class="jcodex-apgwco-style-presets"> 90 <button type="button" class="jcodex-apgwco-preset-btn is-active" data-preset="ecommerce"><?php esc_html_e( 'E-commerce', 'jcodex-ai-product-generator-for-woocommerce' ); ?></button> 91 <button type="button" class="jcodex-apgwco-preset-btn" data-preset="lifestyle"><?php esc_html_e( 'Lifestyle', 'jcodex-ai-product-generator-for-woocommerce' ); ?></button> 92 <button type="button" class="jcodex-apgwco-preset-btn" data-preset="luxury"><?php esc_html_e( 'Luxury', 'jcodex-ai-product-generator-for-woocommerce' ); ?></button> 93 <button type="button" class="jcodex-apgwco-preset-btn" data-preset="minimal"><?php esc_html_e( 'Minimal', 'jcodex-ai-product-generator-for-woocommerce' ); ?></button> 94 94 </div> 95 95 <input type="hidden" id="jcodex-apgwco-style-preset" value="ecommerce" /> -
jcodex-ai-product-generator-for-woocommerce/trunk/includes/class-admin-menu.php
r3724671 r3732385 122 122 } 123 123 124 $pro_url = defined('JCODEX_APGWCO_PRO_URL') ? JCODEX_APGWCO_PRO_URL : 'https://jcodex.com/plugins/jcodex-ai-product-generator-for-woocommerce/'; 125 124 126 $tools = array( 127 array( 128 'title' => __('AI Product Creator', 'jcodex-ai-product-generator-for-woocommerce'), 129 'badge' => __('Pro', 'jcodex-ai-product-generator-for-woocommerce'), 130 'desc' => __('Describe a new product in a few lines. Review the listing, then save it as a WooCommerce draft.', 'jcodex-ai-product-generator-for-woocommerce'), 131 'meta' => __('Available in Pro.', 'jcodex-ai-product-generator-for-woocommerce'), 132 'locked' => true, 133 ), 125 134 array( 126 135 'title' => __('All Products Scanner', 'jcodex-ai-product-generator-for-woocommerce'), … … 139 148 'button' => __('Open brand voice', 'jcodex-ai-product-generator-for-woocommerce'), 140 149 ), 150 array( 151 'title' => __('AI Bulk Generator', 'jcodex-ai-product-generator-for-woocommerce'), 152 'badge' => __('Pro', 'jcodex-ai-product-generator-for-woocommerce'), 153 'desc' => __('Generate descriptions, short descriptions, SEO titles, meta descriptions and tags for many products at once, or export the AI content as CSV.', 'jcodex-ai-product-generator-for-woocommerce'), 154 'meta' => __('Available in Pro.', 'jcodex-ai-product-generator-for-woocommerce'), 155 'locked' => true, 156 ), 141 157 ); 142 158 … … 144 160 * Filter the tool cards shown on the plugin dashboard. 145 161 * 146 * Each item: title, badge (optional), desc, meta (optional), url, button .162 * Each item: title, badge (optional), desc, meta (optional), url, button, locked (optional). 147 163 * 148 164 * @param array $tools Tool cards. … … 161 177 <?php foreach ((array) $tools as $tool): ?> 162 178 <?php 163 if (empty($tool['title']) || empty($tool['url'])) { 179 $locked = !empty($tool['locked']); 180 if (empty($tool['title']) || (!$locked && empty($tool['url']))) { 164 181 continue; 165 182 } 183 $badge_class = 'jcodex-apgwco-dashboard-badge'; 184 if ($locked || (isset($tool['badge']) && __('Pro', 'jcodex-ai-product-generator-for-woocommerce') === $tool['badge'])) { 185 $badge_class .= ' jcodex-apgwco-dashboard-badge-pro'; 186 } 187 $card_class = 'jcodex-apgwco-dashboard-card'; 188 if ($locked) { 189 $card_class .= ' jcodex-apgwco-dashboard-card-locked'; 190 } 166 191 ?> 167 <div class=" jcodex-apgwco-dashboard-card">192 <div class="<?php echo esc_attr($card_class); ?>"> 168 193 <h2> 169 194 <?php echo esc_html($tool['title']); ?> 170 195 <?php if (!empty($tool['badge'])): ?> 171 <span class=" jcodex-apgwco-dashboard-badge"><?php echo esc_html($tool['badge']); ?></span>196 <span class="<?php echo esc_attr($badge_class); ?>"><?php echo esc_html($tool['badge']); ?></span> 172 197 <?php endif; ?> 173 198 </h2> … … 178 203 <p class="jcodex-apgwco-dashboard-meta"><?php echo esc_html($tool['meta']); ?></p> 179 204 <?php endif; ?> 180 <a class="button button-primary" 181 href="<?php echo esc_url($tool['url']); ?>"><?php echo esc_html(!empty($tool['button']) ? $tool['button'] : __('Open', 'jcodex-ai-product-generator-for-woocommerce')); ?></a> 205 <?php if ($locked): ?> 206 <a class="button button-primary" href="<?php echo esc_url($pro_url); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e('Get Pro', 'jcodex-ai-product-generator-for-woocommerce'); ?></a> 207 <?php else: ?> 208 <a class="button button-primary" href="<?php echo esc_url($tool['url']); ?>"><?php echo esc_html(!empty($tool['button']) ? $tool['button'] : __('Open', 'jcodex-ai-product-generator-for-woocommerce')); ?></a> 209 <?php endif; ?> 182 210 </div> 183 211 <?php endforeach; ?> … … 286 314 { 287 315 if ('' !== $this->dashboard_hook && $hook === $this->dashboard_hook) { 288 wp_enqueue_style('jcodex-apgwco-dashboard-css', JCODEX_APGWCO_URL . 'assets/css/dashboard.css', array(), JCODEX_APGWCO_VERSION); 316 $dashboard_css = JCODEX_APGWCO_PATH . 'assets/css/dashboard.css'; 317 wp_enqueue_style( 318 'jcodex-apgwco-dashboard-css', 319 JCODEX_APGWCO_URL . 'assets/css/dashboard.css', 320 array(), 321 file_exists($dashboard_css) ? (string) filemtime($dashboard_css) : JCODEX_APGWCO_VERSION 322 ); 289 323 return; 290 324 } -
jcodex-ai-product-generator-for-woocommerce/trunk/includes/class-admin.php
r3724671 r3732385 79 79 80 80 public function add_setting_links( $links ) { 81 $pro_link = '<a href="' . esc_url( 'https://jcodex.com/plugins/jcodex-ai-product-generator-for-woocommerce/' ) . '" target="_blank" rel="noopener noreferrer" style="color:#1 6a34a;font-weight:600;">' . esc_html__( 'Get Pro', 'jcodex-ai-product-generator-for-woocommerce' ) . '</a>';81 $pro_link = '<a href="' . esc_url( 'https://jcodex.com/plugins/jcodex-ai-product-generator-for-woocommerce/' ) . '" target="_blank" rel="noopener noreferrer" style="color:#1e40af;font-weight:600;">' . esc_html__( 'Get Pro', 'jcodex-ai-product-generator-for-woocommerce' ) . '</a>'; 82 82 $settings_link = '<a href="' . esc_url( admin_url( 'admin.php?page=' . Jcodex_Apgwco_Admin_Menu::MAIN_SLUG ) ) . '">' . esc_html__( 'Settings', 'jcodex-ai-product-generator-for-woocommerce' ) . '</a>'; 83 83 array_unshift( $links, $settings_link ); … … 106 106 ? sprintf( 107 107 /* translators: %s: active SEO plugin name. */ 108 esc_html__( 'Review the draft above, then click "Apply to SEO Fields" to write it into your active SEO plugin (%s). Nothing is changed on your product until you do.', 'jcodex-ai-product-generator-for-woocommerce' ),109 esc_html( $seo_plugin_name )108 __( 'Review the draft above, then click "Apply to SEO Fields" to write it into your active SEO plugin (%s). Nothing is changed on your product until you do.', 'jcodex-ai-product-generator-for-woocommerce' ), 109 $seo_plugin_name 110 110 ) 111 : esc_html__( 'No SEO plugin is active on this site. Click "Apply to SEO Fields" to copy the draft into the "Product SEO Fields" section below, where you can review or edit it — it’s only saved when you click Update/Publish, and it’s then used automatically on the product page.', 'jcodex-ai-product-generator-for-woocommerce' );111 : __( 'No SEO plugin is active on this site. Click "Apply to SEO Fields" to copy the draft into the "Product SEO Fields" section below, where you can review or edit it — it’s only saved when you click Update/Publish, and it’s then used automatically on the product page.', 'jcodex-ai-product-generator-for-woocommerce' ); 112 112 113 113 $current_seo_title = get_post_meta( $post->ID, Jcodex_Apgwco_Product_Health::SEO_TITLE_META, true ); … … 116 116 $current_seo_desc = is_string( $current_seo_desc ) ? trim( $current_seo_desc ) : ''; 117 117 $has_seo_fields = ( '' !== $current_seo_title || '' !== $current_seo_desc ); 118 $health_idle = Jcodex_Apgwco_Product_Health::is_usable_title( get_the_title( $post->ID ) ) 119 ? __( 'Click Check to score this listing and see what is worth fixing.', 'jcodex-ai-product-generator-for-woocommerce' ) 120 : __( 'Name this product first. Health scores a real listing, not an empty draft.', 'jcodex-ai-product-generator-for-woocommerce' ); 118 121 ?> 119 122 … … 123 126 <div class="jcodex-apgwco-header"> 124 127 <div class="jcodex-apgwco-header-left"> 125 <span class="jcodex-apgwco-badge" aria-hidden="true">AI</span> 128 <span class="jcodex-apgwco-badge" aria-hidden="true"> 129 <svg xmlns="http://www.w3.org/2000/svg" width="30" height="32" viewBox="0 0 30 32" fill="none"><path d="M6.45563 0H27.4409L19.7588 24.6342L8.97446 31.6569L0 19.2112L5.61801 14.6161L11.6548 23.1676L13.5604 21.7955L18.5622 5.77489H10.4911L6.45563 0Z" fill="#1B29AE"/></svg> 130 </span> 126 131 <div> 127 <h2><?php esc_html_e( ' ✨All-in-One AI Product Manager', 'jcodex-ai-product-generator-for-woocommerce' ); ?></h2>132 <h2><?php esc_html_e( 'All-in-One AI Product Manager', 'jcodex-ai-product-generator-for-woocommerce' ); ?></h2> 128 133 <p><?php esc_html_e( 'Generate all product content with AI in seconds', 'jcodex-ai-product-generator-for-woocommerce' ); ?></p> 129 134 </div> 130 135 </div> 131 136 <a href="<?php echo esc_url( JCODEX_APGWCO_PRO_URL ); ?>" target="_blank" rel="noopener noreferrer" class="jcodex-apgwco-pro-link"> 132 <?php esc_html_e( ' ⚡Upgrade to Pro', 'jcodex-ai-product-generator-for-woocommerce' ); ?>137 <?php esc_html_e( 'Upgrade to Pro', 'jcodex-ai-product-generator-for-woocommerce' ); ?> 133 138 </a> 134 139 </div> … … 138 143 printf( 139 144 /* translators: %d: number of days until the free monthly usage resets */ 140 esc_html__( ' 🔄Free plan: 10 generations per content type, every month. Resets in %d day(s). Try Regenerate with the same inputs — it won\'t use a credit.', 'jcodex-ai-product-generator-for-woocommerce' ),145 esc_html__( 'Free plan: 10 generations per content type, every month. Resets in %d day(s). Try Regenerate with the same inputs — it won\'t use a credit.', 'jcodex-ai-product-generator-for-woocommerce' ), 141 146 (int) $reset_days 142 147 ); … … 151 156 <div> 152 157 <h3 id="jcodex-apgwco-health-title"><?php esc_html_e( 'AI Product Health', 'jcodex-ai-product-generator-for-woocommerce' ); ?></h3> 153 <p id="jcodex-apgwco-health-summary"><?php e sc_html_e( 'Review this product for practical content, SEO, image, and conversion improvements.', 'jcodex-ai-product-generator-for-woocommerce'); ?></p>158 <p id="jcodex-apgwco-health-summary"><?php echo esc_html( $health_idle ); ?></p> 154 159 </div> 155 160 <div id="jcodex-apgwco-health-score" class="jcodex-apgwco-health-score" aria-live="polite">--<small>/ 100</small></div> 156 161 </div> 157 <button type="button" id="jcodex-apgwco-analyze-health" class="jcodex-apgwco-btn secondary"><?php esc_html_e( 'Analyze Product', 'jcodex-ai-product-generator-for-woocommerce' ); ?></button> 162 <div class="jcodex-apgwco-health-actions"> 163 <button type="button" id="jcodex-apgwco-analyze-health" class="jcodex-apgwco-btn secondary"><?php esc_html_e( 'Check', 'jcodex-ai-product-generator-for-woocommerce' ); ?></button> 164 <a class="jcodex-apgwco-btn primary jcodex-apgwco-btn-premium" href="<?php echo esc_url( defined( 'JCODEX_APGWCO_PRO_URL' ) ? JCODEX_APGWCO_PRO_URL : 'https://jcodex.com/plugins/jcodex-ai-product-generator-for-woocommerce/' ); ?>" target="_blank" rel="noopener noreferrer" title="<?php esc_attr_e( 'Premium feature', 'jcodex-ai-product-generator-for-woocommerce' ); ?>" aria-disabled="true"><?php esc_html_e( 'Improve Product with AI', 'jcodex-ai-product-generator-for-woocommerce' ); ?></a> 165 </div> 158 166 <div id="jcodex-apgwco-health-status" class="jcodex-apgwco-status" role="status" aria-live="polite"></div> 159 167 <div id="jcodex-apgwco-health-results" hidden></div> 168 <div id="jcodex-apgwco-optimize-body" hidden></div> 160 169 </section> 161 170 … … 340 349 <div id="jcodex-apgwco-seo-title-content" class="jcodex-apgwco-output-content jcodex-apgwco-plain-output"></div> 341 350 <div class="jcodex-apgwco-char-counter" id="jcodex-apgwco-seo-title-chars"></div> 342 <p class="description" style="margin-top:6px;"><?php echo $seo_apply_help; ?></p>351 <p class="description" style="margin-top:6px;"><?php echo esc_html( $seo_apply_help ); ?></p> 343 352 </div> 344 353 </div> … … 363 372 <div id="jcodex-apgwco-meta-desc-content" class="jcodex-apgwco-output-content jcodex-apgwco-plain-output"></div> 364 373 <div class="jcodex-apgwco-char-counter" id="jcodex-apgwco-meta-desc-chars"></div> 365 <p class="description" style="margin-top:6px;"><?php echo $seo_apply_help; ?></p>374 <p class="description" style="margin-top:6px;"><?php echo esc_html( $seo_apply_help ); ?></p> 366 375 </div> 367 376 </div> … … 370 379 371 380 <?php if ( ! $seo_plugin_active ) : ?> 372 <div id="jcodex-apgwco-seo-fields-section" class="jcodex-apgwco-seo-fields-section" <?php echo $has_seo_fields ? '' : 'hidden'; ?>>381 <div id="jcodex-apgwco-seo-fields-section" class="jcodex-apgwco-seo-fields-section"<?php echo $has_seo_fields ? '' : ' hidden'; ?>> 373 382 <h4><?php esc_html_e( 'Product SEO Fields', 'jcodex-ai-product-generator-for-woocommerce' ); ?></h4> 374 383 <p class="description"> … … 434 443 JCODEX_APGWCO_URL . 'assets/js/admin.js', 435 444 array( 'jquery', 'media-upload', 'media-views', 'set-post-thumbnail' ), 436 JCODEX_APGWCO_VERSION,445 file_exists( JCODEX_APGWCO_PATH . 'assets/js/admin.js' ) ? (string) filemtime( JCODEX_APGWCO_PATH . 'assets/js/admin.js' ) : JCODEX_APGWCO_VERSION, 437 446 true 438 447 ); … … 446 455 'post_id' => get_the_ID(), 447 456 'remaining' => Jcodex_Apgwco_Limits::get_all_remaining(), 457 'needs_usage_sync' => ! Jcodex_Apgwco_Remote::has_usage_cache(), 448 458 'health' => array( 449 'cached' => ( new Jcodex_Apgwco_Product_Health() )->get_cached( get_the_ID() ),459 'cached' => false, 450 460 ), 451 461 'i18n' => array( … … 470 480 'describeFirst' => esc_html__( 'Describe your product first to enable this AI tool.', 'jcodex-ai-product-generator-for-woocommerce' ), 471 481 'fillFieldFirst' => esc_html__( 'Please fill out this field first', 'jcodex-ai-product-generator-for-woocommerce' ), 472 'analyzing' => esc_html__( 'Analyzing product...', 'jcodex-ai-product-generator-for-woocommerce' ), 473 'analysisComplete' => esc_html__( 'Analysis complete.', 'jcodex-ai-product-generator-for-woocommerce' ), 482 'healthNeedName' => esc_html__( 'Name this product first. Health scores a real listing, not an empty draft.', 'jcodex-ai-product-generator-for-woocommerce' ), 483 'analyzing' => esc_html__( 'Checking…', 'jcodex-ai-product-generator-for-woocommerce' ), 484 'healthCheck' => esc_html__( 'Check', 'jcodex-ai-product-generator-for-woocommerce' ), 485 'healthRecheck' => esc_html__( 'Re-check', 'jcodex-ai-product-generator-for-woocommerce' ), 486 'analysisComplete' => esc_html__( 'Updated.', 'jcodex-ai-product-generator-for-woocommerce' ), 474 487 'analysisFailed' => esc_html__( 'Product analysis could not be completed. Please try again.', 'jcodex-ai-product-generator-for-woocommerce' ), 475 'healthGood' => esc_html__( 'Good — a few improvements can make this product stronger.', 'jcodex-ai-product-generator-for-woocommerce' ), 476 'healthNeedsWork' => esc_html__( 'This product has important opportunities for improvement.', 'jcodex-ai-product-generator-for-woocommerce' ), 477 'priorityIssues' => esc_html__( 'Priority Improvements', 'jcodex-ai-product-generator-for-woocommerce' ), 478 'quickWins' => esc_html__( 'Quick wins', 'jcodex-ai-product-generator-for-woocommerce' ), 488 'healthGood' => esc_html__( 'This listing looks solid.', 'jcodex-ai-product-generator-for-woocommerce' ), 489 /* translators: %d: number of health-score items still to fix. */ 490 'toFix' => esc_html__( '%d to fix', 'jcodex-ai-product-generator-for-woocommerce' ), 491 'allClear' => esc_html__( 'Nothing obvious to fix.', 'jcodex-ai-product-generator-for-woocommerce' ), 492 'manualOnly' => esc_html__( 'These need you: a focus keyword in your SEO plugin, extra photos, or a SKU. Improve with AI writes the rest.', 'jcodex-ai-product-generator-for-woocommerce' ), 479 493 'categoryLabels' => array( 480 494 'product_information' => esc_html__( 'Product Information', 'jcodex-ai-product-generator-for-woocommerce' ), -
jcodex-ai-product-generator-for-woocommerce/trunk/includes/class-api.php
r3724671 r3732385 25 25 /** @var string Usage bucket of the request currently being processed. */ 26 26 private $current_type = 'text'; 27 28 /** @var bool False when this click is a same-prompt free regenerate. */ 29 private $expect_charge = true; 27 30 28 31 public function __construct() { … … 38 41 add_action( 'wp_ajax_jcodex_apgwco_generate_product_image', array( $this, 'generate_product_image' ) ); 39 42 add_action( 'wp_ajax_jcodex_apgwco_analyze_product_health', array( $this, 'analyze_product_health' ) ); 40 }41 42 /** 43 * Analyze a product only when an editor asks for it. Deterministic44 * checks are cached by a product-content checksum; the optional AI45 * review uses the existing text-generation allowance for free users.43 add_action( 'wp_ajax_jcodex_apgwco_sync_usage', array( $this, 'ajax_sync_usage' ) ); 44 } 45 46 /** 47 * Analyze a product only when an editor asks for it. The checklist 48 * is deterministic and unlimited; it never consumes text credits. 46 49 */ 47 50 public function analyze_product_health() { … … 54 57 55 58 $health = new Jcodex_Apgwco_Product_Health(); 56 $cached = $health->get_cached( $product_id ); 57 if ( $cached ) { 58 wp_send_json_success( array( 'analysis' => $cached, 'cached' => true, 'remaining' => Jcodex_Apgwco_Limits::get_remaining( 'text' ) ) ); 59 } 60 61 $analysis = $health->build( $product_id ); 59 $overrides = array(); 60 if ( isset( $_POST['title'] ) ) { 61 $live_title = sanitize_text_field( wp_unslash( $_POST['title'] ) ); 62 if ( Jcodex_Apgwco_Product_Health::is_usable_title( $live_title ) ) { 63 $overrides['title'] = $live_title; 64 } 65 } 66 67 $analysis = $health->build( $product_id, $overrides ); 62 68 if ( is_wp_error( $analysis ) ) { 63 69 wp_send_json_error( array( 'message' => esc_html( $analysis->get_error_message() ) ) ); 64 70 } 65 71 66 $charge_quota = $this->resolve_charge( 'text' );67 $context = $analysis['product_context'];68 72 unset( $analysis['product_context'] ); 69 73 70 $prompt = sprintf( 71 "Analyze this WooCommerce product for quality, SEO, customer clarity, persuasiveness and conversion readiness. Return JSON only: no Markdown, no code fences, no explanation. The JSON must exactly contain overall_score (integer 0-100), categories with product_information, content, seo, images, attributes and conversion; each category must contain score (integer 0-100), issues (array of short strings) and recommendations (array of short strings), plus priority_issues and quick_wins arrays of short strings. Do not invent product facts.\n\nProduct data:\n%s", 72 wp_json_encode( $context ) 73 ); 74 $res = $this->call_remote_api( '/generate-text', array( 'prompt' => $prompt ), false ); 75 76 if ( ! empty( $res['error'] ) ) { 77 // The deterministic result remains useful when the AI service is unavailable. 78 $analysis['ai_warning'] = esc_html__( 'AI insights are temporarily unavailable. Showing the standard product checks instead.', 'jcodex-ai-product-generator-for-woocommerce' ); 79 wp_send_json_success( array( 'analysis' => $analysis, 'cached' => false, 'ai_warning' => true, 'remaining' => Jcodex_Apgwco_Limits::get_remaining( 'text' ) ) ); 80 } 81 82 $ai = $this->parse_health_json( isset( $res['content'] ) ? $res['content'] : '' ); 83 $merged_analysis = is_wp_error( $ai ) ? false : $health->merge_ai_analysis( $analysis, $ai ); 84 if ( false === $merged_analysis ) { 85 $analysis['ai_warning'] = esc_html__( 'AI returned an invalid analysis. Showing the standard product checks instead.', 'jcodex-ai-product-generator-for-woocommerce' ); 86 wp_send_json_success( array( 'analysis' => $analysis, 'cached' => false, 'ai_warning' => true, 'remaining' => Jcodex_Apgwco_Limits::get_remaining( 'text' ) ) ); 87 } 88 89 $analysis = $merged_analysis; 90 $this->finalize_charge( 'text', $charge_quota ); 91 $health->save( $product_id, $analysis ); 74 $db_title = get_the_title( $product_id ); 75 $can_cache = empty( $overrides ) || ( isset( $overrides['title'] ) && $overrides['title'] === $db_title ); 76 if ( $can_cache ) { 77 $health->save( $product_id, $analysis ); 78 } 79 92 80 wp_send_json_success( array( 'analysis' => $analysis, 'cached' => false, 'remaining' => Jcodex_Apgwco_Limits::get_remaining( 'text' ) ) ); 81 } 82 83 /** 84 * Refresh the usage badges after install/reinstall without blocking the editor render. 85 */ 86 public function ajax_sync_usage() { 87 $this->check_permission(); 88 Jcodex_Apgwco_Remote::sync_usage(); 89 wp_send_json_success( array( 'remaining' => Jcodex_Apgwco_Limits::get_all_remaining() ) ); 93 90 } 94 91 … … 128 125 // Signed, per-site request. Quota is enforced on the Jcodex server. 129 126 $timeout = ( '/generate-image' === $endpoint ) ? 120 : 90; 130 return Jcodex_Apgwco_Remote::request( $endpoint, $body, $this->current_type, $timeout ); 127 return Jcodex_Apgwco_Remote::request( $endpoint, $body, $this->current_type, $timeout, true, $this->expect_charge ); 128 } 129 130 /** 131 * Require a non-empty text payload. Escapes the error once for JS .html(). 132 * 133 * @param array $res Remote response. 134 * @return string 135 */ 136 private function require_text_content( $res ) { 137 if ( ! is_array( $res ) ) { 138 wp_send_json_error( array( 139 'message' => esc_html__( 'The AI service could not be reached. Please try again.', 'jcodex-ai-product-generator-for-woocommerce' ), 140 ) ); 141 } 142 if ( ! empty( $res['error'] ) ) { 143 wp_send_json_error( array( 'message' => esc_html( (string) $res['error'] ) ) ); 144 } 145 if ( ! isset( $res['content'] ) || ! is_string( $res['content'] ) || '' === trim( $res['content'] ) ) { 146 wp_send_json_error( array( 147 'message' => esc_html__( 'The AI returned empty content. Please try again.', 'jcodex-ai-product-generator-for-woocommerce' ), 148 ) ); 149 } 150 return $res['content']; 131 151 } 132 152 … … 137 157 check_ajax_referer( 'jcodex_apgwco_nonce', 'nonce' ); 138 158 139 if ( ! current_user_can( 'edit_p osts' ) ) {159 if ( ! current_user_can( 'edit_products' ) ) { 140 160 wp_send_json_error( array( 141 161 'message' => esc_html__( 'You do not have permission to perform this action.', 'jcodex-ai-product-generator-for-woocommerce' ), … … 184 204 */ 185 205 private function resolve_charge( $type ) { 186 $this->current_type = $type; 187 $fingerprint = $this->get_request_fingerprint(); 206 $this->current_type = $type; 207 $this->expect_charge = true; 208 $fingerprint = $this->get_request_fingerprint(); 188 209 189 210 if ( ! Jcodex_Apgwco_Limits::resolve_charge( $type, $fingerprint ) ) { 211 $this->expect_charge = false; 190 212 return false; // Free regenerate — same inputs, within window, not yet used. 191 213 } … … 203 225 */ 204 226 private function finalize_charge( $type, $charge_quota ) { 205 if ( $charge_quota ) {227 if ( $charge_quota && Jcodex_Apgwco_Remote::last_was_charged() ) { 206 228 Jcodex_Apgwco_Limits::increase_usage( $type ); 207 229 } … … 335 357 ); 336 358 337 $res = $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ); 338 339 if ( ! empty( $res['error'] ) ) { 340 wp_send_json_error( array( 'message' => esc_html( $res['error'] ) ) ); 341 } 359 $content = $this->require_text_content( $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ) ); 342 360 343 361 $this->finalize_charge( 'text', $charge_quota ); … … 345 363 wp_send_json_success( array( 346 364 'free_regenerate' => ! $charge_quota, 347 'content' => wp_kses_post( $ res['content']),365 'content' => wp_kses_post( $content ), 348 366 'remaining' => Jcodex_Apgwco_Limits::get_remaining( 'text' ), 349 367 ) ); … … 367 385 ); 368 386 369 $res = $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ); 370 371 if ( ! empty( $res['error'] ) ) { 372 wp_send_json_error( array( 'message' => esc_html( $res['error'] ) ) ); 373 } 387 $content = $this->require_text_content( $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ) ); 374 388 375 389 $this->finalize_charge( 'short_desc', $charge_quota ); … … 377 391 wp_send_json_success( array( 378 392 'free_regenerate' => ! $charge_quota, 379 'content' => wp_kses_post( $ res['content']),393 'content' => wp_kses_post( $content ), 380 394 'remaining' => Jcodex_Apgwco_Limits::get_remaining( 'short_desc' ), 381 395 ) ); … … 397 411 ); 398 412 399 $res = $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ); 400 401 if ( ! empty( $res['error'] ) ) { 402 wp_send_json_error( array( 'message' => esc_html( $res['error'] ) ) ); 403 } 413 $content = $this->require_text_content( $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ) ); 404 414 405 415 $this->finalize_charge( 'features', $charge_quota ); … … 407 417 wp_send_json_success( array( 408 418 'free_regenerate' => ! $charge_quota, 409 'content' => wp_kses_post( $ res['content']),419 'content' => wp_kses_post( $content ), 410 420 'remaining' => Jcodex_Apgwco_Limits::get_remaining( 'features' ), 411 421 ) ); … … 428 438 ); 429 439 430 $res = $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ); 431 432 if ( ! empty( $res['error'] ) ) { 433 wp_send_json_error( array( 'message' => esc_html( $res['error'] ) ) ); 434 } 440 $content = $this->require_text_content( $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ) ); 435 441 436 442 $this->finalize_charge( 'benefits', $charge_quota ); … … 438 444 wp_send_json_success( array( 439 445 'free_regenerate' => ! $charge_quota, 440 'content' => wp_kses_post( $ res['content']),446 'content' => wp_kses_post( $content ), 441 447 'remaining' => Jcodex_Apgwco_Limits::get_remaining( 'benefits' ), 442 448 ) ); … … 458 464 ); 459 465 460 $res = $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ); 461 462 if ( ! empty( $res['error'] ) ) { 463 wp_send_json_error( array( 'message' => esc_html( $res['error'] ) ) ); 464 } 466 $content = $this->require_text_content( $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ) ); 465 467 466 468 // Strip any HTML from tags response 467 $content = wp_strip_all_tags( $ res['content']);469 $content = wp_strip_all_tags( $content ); 468 470 469 471 $this->finalize_charge( 'tags', $charge_quota ); … … 491 493 ); 492 494 493 $res = $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ); 494 495 if ( ! empty( $res['error'] ) ) { 496 wp_send_json_error( array( 'message' => esc_html( $res['error'] ) ) ); 497 } 498 499 $content = sanitize_text_field( wp_strip_all_tags( $res['content'] ) ); 495 $content = sanitize_text_field( wp_strip_all_tags( $this->require_text_content( $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ) ) ) ); 500 496 501 497 $this->finalize_charge( 'seo_title', $charge_quota ); … … 523 519 ); 524 520 525 $res = $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ); 526 527 if ( ! empty( $res['error'] ) ) { 528 wp_send_json_error( array( 'message' => esc_html( $res['error'] ) ) ); 529 } 530 531 $content = sanitize_text_field( wp_strip_all_tags( $res['content'] ) ); 521 $content = sanitize_text_field( wp_strip_all_tags( $this->require_text_content( $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ) ) ) ); 532 522 533 523 $this->finalize_charge( 'meta_desc', $charge_quota ); … … 555 545 ); 556 546 557 $res = $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ); 558 559 if ( ! empty( $res['error'] ) ) { 560 wp_send_json_error( array( 'message' => esc_html( $res['error'] ) ) ); 561 } 562 563 $content = sanitize_textarea_field( wp_strip_all_tags( $res['content'] ) ); 547 $content = sanitize_textarea_field( wp_strip_all_tags( $this->require_text_content( $this->call_remote_api( '/generate-text', array( 'prompt' => $final_prompt ) ) ) ) ); 564 548 565 549 $this->finalize_charge( 'name_improve', $charge_quota ); … … 577 561 578 562 public function generate_product_image() { 579 check_ajax_referer( 'jcodex_apgwco_nonce', 'nonce' ); 580 581 if ( ! current_user_can( 'edit_posts' ) ) { 582 wp_send_json_error( array( 583 'message' => esc_html__( 'You do not have permission to perform this action.', 'jcodex-ai-product-generator-for-woocommerce' ), 584 ) ); 585 } 563 $this->check_permission(); 586 564 587 565 $charge_quota = $this->resolve_charge( 'image' ); … … 618 596 619 597 if ( $source_attachment_id > 0 ) { 620 if ( ! current_user_can( 'read_post', $source_attachment_id ) ) { 598 $can_use_source = current_user_can( 'edit_post', $source_attachment_id ) 599 || ( current_user_can( 'upload_files' ) && ( $post_id < 1 || current_user_can( 'edit_post', $post_id ) ) ); 600 if ( ! $can_use_source ) { 621 601 wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to use this image.', 'jcodex-ai-product-generator-for-woocommerce' ) ) ); 622 602 } … … 652 632 } 653 633 $alt_text = wp_strip_all_tags( $alt_text ); 654 if ( strlen( $alt_text ) > 125 ) { 634 if ( class_exists( 'Jcodex_Apgwco_Language' ) && Jcodex_Apgwco_Language::text_len( $alt_text ) > 125 ) { 635 $alt_text = Jcodex_Apgwco_Language::text_cut( $alt_text, 125 ); 636 } elseif ( strlen( $alt_text ) > 125 ) { 655 637 $alt_text = substr( $alt_text, 0, 125 ); 656 638 } … … 753 735 private function create_optimized_source_file( $original_path, $attachment_id ) { 754 736 require_once ABSPATH . 'wp-admin/includes/image.php'; 737 $upload = wp_get_upload_dir(); 738 if ( ! empty( $upload['error'] ) || empty( $upload['basedir'] ) || ! is_dir( $upload['basedir'] ) ) { 739 return new WP_Error( 'source_too_large', esc_html__( 'Could not prepare the source image.', 'jcodex-ai-product-generator-for-woocommerce' ) ); 740 } 741 755 742 $dimensions = array( self::SOURCE_MAX_DIMENSION, 1600, 1280, 1024, 800 ); 756 743 757 744 foreach ( $dimensions as $max_dim ) { 758 745 $editor = wp_get_image_editor( $original_path ); 759 if ( is_wp_error( $editor ) ) continue; 746 if ( is_wp_error( $editor ) ) { 747 continue; 748 } 760 749 761 750 $editor->resize( $max_dim, $max_dim, false ); 762 751 $editor->set_quality( 82 ); 763 $saved = $editor->save(); 764 765 if ( is_wp_error( $saved ) || empty( $saved['path'] ) || ! is_readable( $saved['path'] ) ) continue; 752 $dest = trailingslashit( $upload['basedir'] ) . wp_unique_filename( $upload['basedir'], 'jcodex-apgwco-src-' . wp_generate_password( 8, false ) . '.jpg' ); 753 $saved = $editor->save( $dest, 'image/jpeg' ); 754 755 if ( is_wp_error( $saved ) || empty( $saved['path'] ) || ! is_readable( $saved['path'] ) ) { 756 continue; 757 } 766 758 767 759 $filesize = filesize( $saved['path'] ); 768 760 if ( false !== $filesize && $filesize > 0 && $filesize <= self::SOURCE_MAX_BYTES ) { 769 $mime = ! empty( $saved['mime-type'] ) ? $saved['mime-type'] : $this->get_image_mime_type( $saved['path'], $attachment_id );761 $mime = ! empty( $saved['mime-type'] ) ? $saved['mime-type'] : 'image/jpeg'; 770 762 return array( 'path' => $saved['path'], 'mime' => $mime, 'temp' => true ); 771 763 } … … 784 776 } 785 777 786 $allowed = array( 'image/jpeg', 'image/png', 'image/webp' , 'image/gif');778 $allowed = array( 'image/jpeg', 'image/png', 'image/webp' ); 787 779 if ( ! in_array( $resolved['mime'], $allowed, true ) ) { 788 780 return array( 'error' => esc_html__( 'Please upload a JPEG, PNG, or WebP image.', 'jcodex-ai-product-generator-for-woocommerce' ) ); … … 836 828 // 2. PHP getimagesizefromstring 837 829 if ( empty( $mime ) && function_exists( 'getimagesizefromstring' ) ) { 838 $img_info = @getimagesizefromstring( $image_data );830 $img_info = getimagesizefromstring( $image_data ); 839 831 if ( ! empty( $img_info['mime'] ) ) { 840 832 $mime = $img_info['mime']; -
jcodex-ai-product-generator-for-woocommerce/trunk/includes/class-brand-voice.php
r3724671 r3732385 451 451 452 452 $text = trim( preg_replace( '/\s+/u', ' ', wp_strip_all_tags( strip_shortcodes( $post->post_content ) ) ) ); 453 if ( str_word_count( $text ) < 30 ) {453 if ( Jcodex_Apgwco_Language::word_count( $text ) < 30 ) { 454 454 continue; 455 455 } … … 457 457 $samples[] = array( 458 458 'title' => wp_strip_all_tags( get_the_title( $product_id ) ), 459 'text' => wp_trim_words( $text, 90, ''),459 'text' => Jcodex_Apgwco_Language::trim_words( $text, 90 ), 460 460 ); 461 461 } -
jcodex-ai-product-generator-for-woocommerce/trunk/includes/class-content-scanner.php
r3724671 r3732385 652 652 } 653 653 654 if ( ! $has_faq && preg_match( '/faq|frequently\s+asked\s+questions/i', wp_strip_all_tags( $raw_content ) ) ) { 654 if ( ! $has_faq && class_exists( 'Jcodex_Apgwco_Language' ) ) { 655 $plain = wp_strip_all_tags( $raw_content ); 656 foreach ( Jcodex_Apgwco_Language::faq_text_markers() as $marker ) { 657 if ( Jcodex_Apgwco_Language::text_contains( $plain, $marker ) ) { 658 $has_faq = true; 659 break; 660 } 661 } 662 } elseif ( ! $has_faq && preg_match( '/faq|frequently\s+asked\s+questions/i', wp_strip_all_tags( $raw_content ) ) ) { 655 663 $has_faq = true; 656 664 } … … 969 977 } 970 978 979 $owner = isset( $state['user_id'] ) ? (int) $state['user_id'] : 0; 980 if ( $owner && $owner !== get_current_user_id() && ! current_user_can( 'manage_woocommerce' ) ) { 981 wp_send_json_error( array( 'message' => esc_html__( 'This scan is no longer active. Please start a new scan.', 'jcodex-ai-product-generator-for-woocommerce' ) ) ); 982 } 983 971 984 return $state; 972 985 } -
jcodex-ai-product-generator-for-woocommerce/trunk/includes/class-language.php
r3724671 r3732385 101 101 'fr_CA' => 'Canadian French', 102 102 'de_CH' => 'Swiss German', 103 'pa' => 'Punjabi', 104 'pa_IN' => 'Punjabi (India)', 105 'sw' => 'Swahili', 106 ); 107 } 108 109 /** 110 * Character length that treats å, ä, ö, Urdu, and Arabic as one character each. 111 * 112 * @param string $text Text. 113 * @return int 114 */ 115 public static function text_len( $text ) { 116 $text = (string) $text; 117 if ( function_exists( 'mb_strlen' ) ) { 118 return (int) mb_strlen( $text, 'UTF-8' ); 119 } 120 if ( function_exists( 'iconv_strlen' ) ) { 121 $len = @iconv_strlen( $text, 'UTF-8' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged 122 if ( false !== $len ) { 123 return (int) $len; 124 } 125 } 126 return strlen( $text ); 127 } 128 129 /** 130 * Lowercase that understands non-ASCII letters. 131 * 132 * @param string $text Text. 133 * @return string 134 */ 135 public static function text_lower( $text ) { 136 $text = (string) $text; 137 if ( function_exists( 'mb_strtolower' ) ) { 138 return mb_strtolower( $text, 'UTF-8' ); 139 } 140 return strtolower( $text ); 141 } 142 143 /** 144 * Cut a string to a character count without splitting a multibyte glyph. 145 * 146 * @param string $text Text. 147 * @param int $max Max characters. 148 * @return string 149 */ 150 public static function text_cut( $text, $max ) { 151 $text = (string) $text; 152 $max = (int) $max; 153 if ( $max < 1 || self::text_len( $text ) <= $max ) { 154 return $text; 155 } 156 if ( function_exists( 'mb_substr' ) ) { 157 return mb_substr( $text, 0, $max, 'UTF-8' ); 158 } 159 return substr( $text, 0, $max ); 160 } 161 162 /** 163 * Case-insensitive substring match for UTF-8 text. 164 * 165 * @param string $haystack Haystack. 166 * @param string $needle Needle. 167 * @return bool 168 */ 169 public static function text_contains( $haystack, $needle ) { 170 $haystack = self::text_lower( $haystack ); 171 $needle = self::text_lower( $needle ); 172 if ( '' === $needle ) { 173 return false; 174 } 175 if ( function_exists( 'mb_strpos' ) ) { 176 return false !== mb_strpos( $haystack, $needle, 0, 'UTF-8' ); 177 } 178 return false !== strpos( $haystack, $needle ); 179 } 180 181 /** 182 * Word count that works for Latin, Arabic, Urdu, and CJK. 183 * 184 * @param string $text Text. 185 * @return int 186 */ 187 public static function word_count( $text ) { 188 $text = trim( (string) $text ); 189 if ( '' === $text ) { 190 return 0; 191 } 192 if ( preg_match( '/[\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]/u', $text ) ) { 193 $copy = preg_replace( '/[^\p{L}\p{N}]+/u', '', $text ); 194 $len = self::text_len( is_string( $copy ) ? $copy : $text ); 195 return (int) max( 1, (int) floor( $len / 2 ) ); 196 } 197 if ( preg_match_all( '/[\p{L}\p{N}]+/u', $text, $matches ) ) { 198 return count( $matches[0] ); 199 } 200 return 0; 201 } 202 203 /** 204 * Trim to N words (or ~2N CJK characters) without str_word_count(). 205 * 206 * @param string $text Text. 207 * @param int $num Word limit. 208 * @return string 209 */ 210 public static function trim_words( $text, $num ) { 211 $text = trim( (string) $text ); 212 $num = max( 1, (int) $num ); 213 if ( '' === $text ) { 214 return ''; 215 } 216 if ( preg_match( '/[\p{Han}\p{Hiragana}\p{Katakana}\p{Hangul}]/u', $text ) ) { 217 return self::text_cut( $text, $num * 2 ); 218 } 219 if ( ! preg_match_all( '/[\p{L}\p{N}]+/u', $text, $matches, PREG_OFFSET_CAPTURE ) ) { 220 return $text; 221 } 222 if ( count( $matches[0] ) <= $num ) { 223 return $text; 224 } 225 $last = $matches[0][ $num - 1 ]; 226 $end = (int) $last[1] + strlen( $last[0] ); 227 return rtrim( substr( $text, 0, $end ) ); 228 } 229 230 /** 231 * Language-agnostic FAQ heading / phrase markers. 232 * 233 * @return string[] 234 */ 235 public static function faq_text_markers() { 236 return array( 237 'faq', 238 'frequently asked', 239 '"FAQPage"', 240 '<details', 241 'vanliga frågor', 242 'frågor och svar', 243 'häufige fragen', 244 'häufig gestellte', 245 'preguntas frecuentes', 246 'questions fréquentes', 247 'foire aux questions', 248 'perguntas frequentes', 249 'domande frequenti', 250 'veelgestelde vragen', 251 'często zadawane', 252 'sık sorulan', 253 'часто задаваем', 254 'الأسئلة الشائعة', 255 'الاسئلة الشائعة', 256 'سوالات متداول', 257 'عمومی سوالات', 258 'اکثر پوچھے گئے', 259 '常见问题', 260 '常見問題', 261 'よくある質問', 262 '자주 묻는', 263 'คำถามที่พบบ่อย', 264 ); 265 } 266 267 /** 268 * Language-agnostic benefit / "why buy" markers. 269 * 270 * @return string[] 271 */ 272 public static function benefits_text_markers() { 273 return array( 274 'benefit', 275 'benefits', 276 "you'll love", 277 'you will love', 278 'why you', 279 'fördel', 280 'fördelar', 281 'vorteil', 282 'vorteile', 283 'avantage', 284 'avantages', 285 'ventaja', 286 'ventajas', 287 'voordeel', 288 'voordelen', 289 'fordel', 290 'fordeler', 291 'vantaggi', 292 'korzyść', 293 'korzyści', 294 'avantaj', 295 'преимуществ', 296 'выгод', 297 'فوائد', 298 'فائدة', 299 'فائدے', 300 'ประโยชน์', 301 '优点', 302 '好處', 303 '好处', 304 'メリット', 305 '利点', 306 '장점', 307 '혜택', 308 'hyöty', 309 'hyödyt', 103 310 ); 104 311 } -
jcodex-ai-product-generator-for-woocommerce/trunk/includes/class-limits.php
r3724671 r3732385 40 40 } 41 41 42 /** Usage is counted on the server; nothing to do here. */42 /** Decrement the local badge cache after a chargeable generate. */ 43 43 public static function increase_usage( $type = 'text' ) { 44 return;44 Jcodex_Apgwco_Remote::ensure_type_decremented( $type ); 45 45 } 46 46 … … 107 107 return; 108 108 } 109 set_transient( self::get_regen_transient_key( $type ), array( 'hash' => $fingerprint ), self::REGENERATE_WINDOW_SECONDS ); 109 $key = self::get_regen_transient_key( $type ); 110 $cached = get_transient( $key ); 111 if ( is_array( $cached ) && ! empty( $cached['hash'] ) && $cached['hash'] === $fingerprint ) { 112 $started = isset( $cached['started'] ) ? (int) $cached['started'] : 0; 113 if ( $started > 0 ) { 114 $left = self::REGENERATE_WINDOW_SECONDS - ( time() - $started ); 115 if ( $left > 0 ) { 116 set_transient( $key, array( 'hash' => $fingerprint, 'started' => $started ), $left ); 117 return; 118 } 119 } else { 120 return; 121 } 122 } 123 set_transient( $key, array( 'hash' => $fingerprint, 'started' => time() ), self::REGENERATE_WINDOW_SECONDS ); 110 124 } 111 125 } -
jcodex-ai-product-generator-for-woocommerce/trunk/includes/class-product-health.php
r3701858 r3732385 66 66 } 67 67 68 public function build( $product_id ) { 68 /** 69 * Score a product. 70 * 71 * @param int $product_id Product ID. 72 * @param array $overrides Optional replacement values, used by the 73 * optimizer to score content that has been 74 * proposed but not saved yet. Accepts title, 75 * description, short_description, seo_title, 76 * meta_description and tags. 77 */ 78 public function build( $product_id, $overrides = array() ) { 69 79 $product = wc_get_product( $product_id ); 70 80 if ( ! $product ) { … … 73 83 74 84 $post = get_post( $product_id ); 85 $raw_content = $post ? (string) $post->post_content : ''; 75 86 $title = trim( get_the_title( $product_id ) ); 76 $description = trim( wp_strip_all_tags( $post ? $post->post_content : '' ) ); 87 if ( ! self::is_usable_title( $title ) ) { 88 $title = ''; 89 } 90 $description = trim( wp_strip_all_tags( $raw_content ) ); 77 91 $short_desc = trim( wp_strip_all_tags( $post ? $post->post_excerpt : '' ) ); 78 92 $categories = wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'ids' ) ); … … 81 95 $image_ids = array_filter( array_merge( array( $product->get_image_id() ), $gallery_ids ) ); 82 96 $seo = $this->get_seo_meta( $product_id ); 97 $focus_keyword = $this->get_focus_keyword( $product_id ); 98 $has_faq = $this->product_has_faq( $product_id, $raw_content ); 99 $has_benefits = $this->product_has_benefits( $description ); 100 101 if ( ! empty( $overrides ) && is_array( $overrides ) ) { 102 if ( isset( $overrides['title'] ) ) { 103 $title = trim( wp_strip_all_tags( (string) $overrides['title'] ) ); 104 if ( ! self::is_usable_title( $title ) ) { 105 $title = ''; 106 } 107 } 108 if ( isset( $overrides['description'] ) ) { 109 $description = trim( wp_strip_all_tags( (string) $overrides['description'] ) ); 110 $has_benefits = $this->product_has_benefits( $description ); 111 } 112 if ( isset( $overrides['short_description'] ) ) { 113 $short_desc = trim( wp_strip_all_tags( (string) $overrides['short_description'] ) ); 114 } 115 if ( isset( $overrides['seo_title'] ) ) { 116 $seo['title'] = trim( wp_strip_all_tags( (string) $overrides['seo_title'] ) ); 117 } 118 if ( isset( $overrides['meta_description'] ) ) { 119 $seo['description'] = trim( wp_strip_all_tags( (string) $overrides['meta_description'] ) ); 120 } 121 if ( isset( $overrides['tags'] ) ) { 122 $tags = array_filter( (array) $overrides['tags'] ); 123 } 124 if ( isset( $overrides['has_faq'] ) ) { 125 $has_faq = (bool) $overrides['has_faq']; 126 } 127 if ( isset( $overrides['has_benefits'] ) ) { 128 $has_benefits = (bool) $overrides['has_benefits']; 129 } 130 if ( isset( $overrides['focus_keyword'] ) ) { 131 $focus_keyword = trim( wp_strip_all_tags( (string) $overrides['focus_keyword'] ) ); 132 } 133 } 134 135 $keyword_used = $this->keyword_is_used( 136 $focus_keyword, 137 array( $title, $description, $short_desc, $seo['title'], $seo['description'] ) 138 ); 139 83 140 $categories_out = array(); 84 141 85 142 $categories_out['product_information'] = $this->score_product_information( $product, $title, $categories ); 86 $categories_out['content'] = $this->score_content( $description, $short_desc );87 $categories_out['seo'] = $this->score_seo( $seo, $tags );143 $categories_out['content'] = $this->score_content( $description, $short_desc, $has_benefits, $has_faq ); 144 $categories_out['seo'] = $this->score_seo( $seo, $tags, $focus_keyword, $keyword_used ); 88 145 $categories_out['images'] = $this->score_images( $product->get_image_id(), $image_ids ); 89 $categories_out['attributes'] = $this->score_attributes( $product ); 90 $categories_out['conversion'] = $this->score_conversion( $product, $short_desc ); 146 $categories_out['attributes'] = ( ! empty( $overrides['has_attributes'] ) ) 147 ? $this->category( 100 ) 148 : $this->score_attributes( $product ); 149 $categories_out['conversion'] = $this->score_conversion( $product, $short_desc, $title, $has_benefits, $has_faq ); 91 150 92 151 return array( 93 'overall_score' => $this->get_overall_score( $ categories_out),152 'overall_score' => $this->get_overall_score( $this->visible_categories( $categories_out ) ), 94 153 'categories' => $categories_out, 95 154 'priority_issues' => $this->get_priority_issues( $categories_out ), 96 155 'quick_wins' => $this->get_quick_wins( $categories_out ), 156 'checklist' => $this->build_checklist( $title, $description, $short_desc, $seo, $focus_keyword, $keyword_used, $has_benefits, $has_faq, $product ), 97 157 'product_context' => array( 98 158 'title' => $title, … … 101 161 'seo_title' => $seo['title'], 102 162 'meta_description' => $seo['description'], 163 'focus_keyword' => $focus_keyword, 103 164 ), 104 165 ); … … 129 190 } 130 191 131 $analysis['overall_score'] = $this->get_overall_score( $ analysis['categories']);192 $analysis['overall_score'] = $this->get_overall_score( $this->visible_categories( $analysis['categories'] ) ); 132 193 $analysis['priority_issues'] = $this->get_priority_issues( $analysis['categories'] ); 133 194 $analysis['quick_wins'] = $this->get_quick_wins( $analysis['categories'] ); … … 138 199 $product = wc_get_product( $product_id ); 139 200 $post = get_post( $product_id ); 140 if ( ! $product || ! $post ) return ''; 141 return md5( wp_json_encode( array( 142 $post->post_title, $post->post_content, $post->post_excerpt, $product->get_sku(), $product->get_price(), 143 $product->get_stock_status(), $product->get_image_id(), $product->get_gallery_image_ids(), $product->get_attributes(), 144 wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'ids' ) ), wp_get_post_terms( $product_id, 'product_tag', array( 'fields' => 'ids' ) ), $this->get_seo_meta( $product_id ), 145 ) ) ); 201 if ( ! $product || ! $post ) { 202 return ''; 203 } 204 205 $image_ids = array_filter( array_merge( array( $product->get_image_id() ), $product->get_gallery_image_ids() ) ); 206 $alts = array(); 207 foreach ( $image_ids as $image_id ) { 208 $alts[ (int) $image_id ] = (string) get_post_meta( $image_id, '_wp_attachment_image_alt', true ); 209 } 210 211 $attributes = array(); 212 foreach ( (array) $product->get_attributes() as $key => $attribute ) { 213 if ( is_object( $attribute ) && method_exists( $attribute, 'get_data' ) ) { 214 $data = $attribute->get_data(); 215 $attributes[ (string) $key ] = array( 216 'name' => isset( $data['name'] ) ? (string) $data['name'] : '', 217 'options' => isset( $data['options'] ) ? array_values( (array) $data['options'] ) : array(), 218 'visible' => ! empty( $data['visible'] ), 219 'variation' => ! empty( $data['variation'] ), 220 ); 221 } elseif ( is_array( $attribute ) ) { 222 $attributes[ (string) $key ] = $attribute; 223 } else { 224 $attributes[ (string) $key ] = (string) $attribute; 225 } 226 } 227 ksort( $attributes ); 228 229 return md5( 230 wp_json_encode( 231 array( 232 $post->post_title, 233 $post->post_content, 234 $post->post_excerpt, 235 $product->get_sku(), 236 $product->get_price(), 237 $product->get_stock_status(), 238 $product->get_image_id(), 239 $product->get_gallery_image_ids(), 240 $attributes, 241 $alts, 242 wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'ids' ) ), 243 wp_get_post_terms( $product_id, 'product_tag', array( 'fields' => 'ids' ) ), 244 $this->get_seo_meta( $product_id ), 245 get_post_meta( $product_id, '_jcodex_apgwco_faq_schema', true ), 246 $this->get_focus_keyword( $product_id ), 247 ) 248 ) 249 ); 250 } 251 252 /** 253 * WooCommerce auto-drafts are titled "AUTO-DRAFT". Scoring or 254 * generating copy from that string produces junk listings. 255 * 256 * @param string $title Raw product title. 257 * @return bool 258 */ 259 public static function is_usable_title( $title ) { 260 $title = trim( wp_strip_all_tags( (string) $title ) ); 261 if ( '' === $title ) { 262 return false; 263 } 264 265 $norm = strtolower( $title ); 266 $norm = str_replace( array( '-', '_' ), ' ', $norm ); 267 $norm = preg_replace( '/\s+/', ' ', $norm ); 268 $norm = is_string( $norm ) ? trim( $norm ) : ''; 269 270 if ( '' === $norm || in_array( $norm, array( 'auto draft', 'draft' ), true ) ) { 271 return false; 272 } 273 274 return 0 !== strpos( $norm, 'auto draft' ); 275 } 276 277 /** 278 * Cached score, or a fresh build, for the product editor. 279 * Auto-drafts and empty titles return false so the panel stays empty. 280 * 281 * @param int $product_id Product ID. 282 * @return array|false 283 */ 284 public function get_or_build( $product_id ) { 285 $product_id = absint( $product_id ); 286 if ( $product_id < 1 || ! self::is_usable_title( get_the_title( $product_id ) ) ) { 287 return false; 288 } 289 290 $cached = $this->get_cached( $product_id ); 291 if ( $cached ) { 292 return $cached; 293 } 294 295 $analysis = $this->build( $product_id ); 296 if ( is_wp_error( $analysis ) ) { 297 return false; 298 } 299 300 unset( $analysis['product_context'] ); 301 $this->save( $product_id, $analysis ); 302 return $analysis; 146 303 } 147 304 … … 151 308 $score = 100; $issues = array(); 152 309 if ( '' === $title ) { $score -= 40; $issues[] = $this->issue( __( 'Product title is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'name' ); } 153 elseif ( strlen( $title ) < 15 ) { $score -= 15; $issues[] = $this->issue( __( 'Product title is very short.', 'jcodex-ai-product-generator-for-woocommerce' ), 'name' ); }310 elseif ( $this->text_len( $title ) < 15 ) { $score -= 15; $issues[] = $this->issue( __( 'Product title is very short.', 'jcodex-ai-product-generator-for-woocommerce' ), 'name' ); } 154 311 if ( '' === $product->get_sku() ) { $score -= 15; $issues[] = $this->issue( __( 'SKU is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'sku' ); } 155 312 if ( empty( $categories ) || is_wp_error( $categories ) ) { $score -= 20; $issues[] = $this->issue( __( 'Product category is missing.', 'jcodex-ai-product-generator-for-woocommerce' ) ); } … … 157 314 return $this->category( $score, $issues ); 158 315 } 159 private function score_content( $description, $short_desc ) { 160 $score = 100; $issues = array(); 161 if ( '' === $description ) { $score -= 55; $issues[] = $this->issue( __( 'Product description is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'description' ); } 162 elseif ( strlen( $description ) < 250 ) { $score -= 25; $issues[] = $this->issue( __( 'Product description is too short.', 'jcodex-ai-product-generator-for-woocommerce' ), 'description' ); } 163 if ( '' === $short_desc ) { $score -= 25; $issues[] = $this->issue( __( 'Short description is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'short_desc' ); } 316 private function score_content( $description, $short_desc, $has_benefits, $has_faq ) { 317 $score = 100; 318 $issues = array(); 319 if ( '' === $description ) { 320 $score -= 40; 321 $issues[] = $this->issue( __( 'Product description is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'description' ); 322 } elseif ( $this->text_len( $description ) < 250 ) { 323 $score -= 20; 324 $issues[] = $this->issue( __( 'Product description is too short.', 'jcodex-ai-product-generator-for-woocommerce' ), 'description' ); 325 } 326 if ( '' === $short_desc ) { 327 $score -= 15; 328 $issues[] = $this->issue( __( 'Short description is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'short_desc' ); 329 } 330 if ( ! $has_benefits ) { 331 $score -= 15; 332 $issues[] = $this->issue( __( 'Benefits are missing from the description.', 'jcodex-ai-product-generator-for-woocommerce' ), 'description' ); 333 } 334 if ( ! $has_faq ) { 335 $score -= 10; 336 $issues[] = $this->issue( __( 'FAQ is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'faq' ); 337 } 164 338 return $this->category( $score, $issues ); 165 339 } 166 private function score_seo( $seo, $tags ) { 167 $score = 100; $issues = array(); 168 if ( '' === $seo['title'] ) { $score -= 35; $issues[] = $this->issue( __( 'SEO title is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'seo_title' ); } 169 if ( '' === $seo['description'] ) { $score -= 40; $issues[] = $this->issue( __( 'Meta description is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'meta_desc' ); } 170 if ( empty( $tags ) || is_wp_error( $tags ) ) { $score -= 20; $issues[] = $this->issue( __( 'Product tags are missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'tags' ); } 340 private function score_seo( $seo, $tags, $focus_keyword, $keyword_used ) { 341 $score = 100; 342 $issues = array(); 343 if ( '' === $seo['title'] ) { 344 $score -= 25; 345 $issues[] = $this->issue( __( 'SEO title is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'seo_title' ); 346 } 347 $meta_len = $this->text_len( $seo['description'] ); 348 if ( 0 === $meta_len ) { 349 $score -= 25; 350 $issues[] = $this->issue( __( 'Meta description is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'meta_desc' ); 351 } elseif ( $meta_len < 120 || $meta_len > 160 ) { 352 $score -= 10; 353 $issues[] = $this->issue( __( 'Meta description is outside the 120–160 character range.', 'jcodex-ai-product-generator-for-woocommerce' ), 'meta_desc' ); 354 } 355 if ( '' === $focus_keyword ) { 356 $score -= 15; 357 $issues[] = $this->issue( __( 'Focus keyword is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'seo_title' ); 358 } elseif ( ! $keyword_used ) { 359 $score -= 10; 360 $issues[] = $this->issue( __( 'Focus keyword is not used in the listing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'description' ); 361 } 171 362 return $this->category( $score, $issues ); 172 363 } … … 180 371 } 181 372 private function score_attributes( $product ) { $attributes = $product->get_attributes(); return empty( $attributes ) ? $this->category( 45, array( $this->issue( __( 'Product attributes are missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'attributes' ) ) ) : $this->category( 100 ); } 182 private function score_conversion( $product, $short_desc ) { $score = 100; $issues = array(); if ( '' === $product->get_price() && ! $product->is_type( 'variable' ) ) { $score -= 35; $issues[] = $this->issue( __( 'A price is needed before customers can buy this product.', 'jcodex-ai-product-generator-for-woocommerce' ) ); } if ( 'outofstock' === $product->get_stock_status() ) { $score -= 25; $issues[] = $this->issue( __( 'Product is out of stock.', 'jcodex-ai-product-generator-for-woocommerce' ) ); } if ( '' === $short_desc ) { $score -= 20; $issues[] = $this->issue( __( 'Add a short description to communicate the key buying benefit.', 'jcodex-ai-product-generator-for-woocommerce' ), 'short_desc' ); } return $this->category( $score, $issues ); } 373 private function score_conversion( $product, $short_desc, $title, $has_benefits, $has_faq ) { 374 $score = 100; 375 $issues = array(); 376 if ( '' === $product->get_price() && ! $product->is_type( 'variable' ) ) { 377 $score -= 25; 378 $issues[] = $this->issue( __( 'Product price is missing.', 'jcodex-ai-product-generator-for-woocommerce' ), 'price' ); 379 } 380 if ( $this->text_len( $title ) < 15 ) { 381 $score -= 25; 382 $issues[] = $this->issue( __( 'Product title is too weak to convert.', 'jcodex-ai-product-generator-for-woocommerce' ), 'name' ); 383 } 384 unset( $short_desc, $has_benefits, $has_faq ); 385 return $this->category( $score, $issues ); 386 } 387 388 private function get_focus_keyword( $product_id ) { 389 $keys = array( 390 '_yoast_wpseo_focuskw', 391 'rank_math_focus_keyword', 392 '_seopress_analysis_target_kw', 393 '_wds_focus-keywords', 394 'wds_focus_keywords', 395 '_tsf_focus_keyword', 396 'focus_keyword', 397 ); 398 399 foreach ( $keys as $meta_key ) { 400 $value = trim( (string) get_post_meta( $product_id, $meta_key, true ) ); 401 if ( '' === $value ) { 402 continue; 403 } 404 $parts = explode( ',', $value ); 405 $first = trim( (string) $parts[0] ); 406 if ( '' !== $first ) { 407 return $first; 408 } 409 } 410 411 $aioseo = $this->get_aioseo_focus_keyword( $product_id ); 412 if ( '' !== $aioseo ) { 413 return $aioseo; 414 } 415 416 return ''; 417 } 418 419 /** 420 * AIOSEO stores the focus keyphrase on its post model, not in post meta. 421 * 422 * @param int $product_id Product ID. 423 * @return string 424 */ 425 private function get_aioseo_focus_keyword( $product_id ) { 426 $class = '\\AIOSEO\\Plugin\\Common\\Models\\Post'; 427 if ( ! class_exists( $class ) || ! method_exists( $class, 'getPost' ) ) { 428 return ''; 429 } 430 431 try { 432 $model = $class::getPost( absint( $product_id ) ); 433 } catch ( Exception $exception ) { 434 return ''; 435 } 436 437 if ( ! is_object( $model ) || ! isset( $model->keyphrases ) ) { 438 return ''; 439 } 440 441 $data = $model->keyphrases; 442 if ( is_string( $data ) ) { 443 $data = json_decode( $data, true ); 444 } elseif ( is_object( $data ) ) { 445 $data = (array) $data; 446 } 447 448 if ( ! is_array( $data ) ) { 449 return ''; 450 } 451 452 $focus = ''; 453 if ( isset( $data['focus']['keyphrase'] ) ) { 454 $focus = (string) $data['focus']['keyphrase']; 455 } elseif ( isset( $data['focus'] ) && is_object( $data['focus'] ) && isset( $data['focus']->keyphrase ) ) { 456 $focus = (string) $data['focus']->keyphrase; 457 } 458 459 return trim( wp_strip_all_tags( $focus ) ); 460 } 461 462 private function keyword_is_used( $keyword, $haystacks ) { 463 $keyword = class_exists( 'Jcodex_Apgwco_Language' ) 464 ? Jcodex_Apgwco_Language::text_lower( trim( (string) $keyword ) ) 465 : strtolower( trim( (string) $keyword ) ); 466 if ( '' === $keyword ) { 467 return false; 468 } 469 foreach ( (array) $haystacks as $haystack ) { 470 if ( class_exists( 'Jcodex_Apgwco_Language' ) ) { 471 if ( Jcodex_Apgwco_Language::text_contains( (string) $haystack, $keyword ) ) { 472 return true; 473 } 474 } elseif ( false !== strpos( strtolower( (string) $haystack ), $keyword ) ) { 475 return true; 476 } 477 } 478 return false; 479 } 480 481 private function product_has_benefits( $description ) { 482 $text = (string) $description; 483 if ( '' === trim( wp_strip_all_tags( $text ) ) ) { 484 return false; 485 } 486 487 if ( preg_match_all( '/<li\b/i', $text ) >= 3 ) { 488 return true; 489 } 490 491 $plain = wp_strip_all_tags( $text ); 492 if ( class_exists( 'Jcodex_Apgwco_Language' ) ) { 493 foreach ( Jcodex_Apgwco_Language::benefits_text_markers() as $marker ) { 494 if ( Jcodex_Apgwco_Language::text_contains( $plain, $marker ) ) { 495 return true; 496 } 497 } 498 } elseif ( preg_match( '/\b(benefit|benefits|why you.?ll|why you will|you.?ll love)\b/i', $plain ) ) { 499 return true; 500 } 501 502 return (bool) apply_filters( 'jcodex_apgwco_product_has_benefits', false, $text ); 503 } 504 505 private function product_has_faq( $product_id, $raw_content ) { 506 if ( class_exists( 'Jcodex_Apgwco_Optimizer' ) ) { 507 return Jcodex_Apgwco_Optimizer::has_faq( $product_id, $raw_content ); 508 } 509 510 $saved = get_post_meta( $product_id, '_jcodex_apgwco_faq_schema', true ); 511 if ( is_array( $saved ) && ! empty( $saved ) ) { 512 return true; 513 } 514 515 return (bool) preg_match( '/faq|frequently\s+asked/i', (string) $raw_content ); 516 } 517 518 private function build_checklist( $title, $description, $short_desc, $seo, $focus_keyword, $keyword_used, $has_benefits, $has_faq, $product ) { 519 $featured_id = $product ? $product->get_image_id() : 0; 520 $image_ids = $product ? array_filter( array_merge( array( $featured_id ), $product->get_gallery_image_ids() ) ) : array(); 521 $meta_len = $this->text_len( $seo['description'] ); 522 $missing_alt = false; 523 foreach ( (array) $image_ids as $image_id ) { 524 if ( '' === trim( (string) get_post_meta( $image_id, '_wp_attachment_image_alt', true ) ) ) { 525 $missing_alt = true; 526 break; 527 } 528 } 529 530 $item = function( $label, $status, $fixable = true ) { 531 return array( 532 'label' => $label, 533 'status' => $status, 534 'fixable' => (bool) $fixable, 535 ); 536 }; 537 538 $title_status = '' === $title ? 'missing' : ( $this->text_len( $title ) < 15 ? 'weak' : 'ok' ); 539 $desc_status = '' === $description ? 'missing' : ( $this->text_len( $description ) < 250 ? 'weak' : 'ok' ); 540 $meta_status = 0 === $meta_len ? 'missing' : ( $meta_len < 120 || $meta_len > 160 ? 'weak' : 'ok' ); 541 $image_status = empty( $image_ids ) ? 'missing' : ( count( $image_ids ) < 2 ? 'weak' : 'ok' ); 542 543 $desc_label = 'weak' === $desc_status 544 ? __( 'Long description too short', 'jcodex-ai-product-generator-for-woocommerce' ) 545 : __( 'Long description', 'jcodex-ai-product-generator-for-woocommerce' ); 546 547 $meta_label = 'weak' === $meta_status 548 ? ( $meta_len > 160 549 ? __( 'Meta description too long', 'jcodex-ai-product-generator-for-woocommerce' ) 550 : __( 'Meta description too short', 'jcodex-ai-product-generator-for-woocommerce' ) ) 551 : __( 'Meta description', 'jcodex-ai-product-generator-for-woocommerce' ); 552 553 $keyword_label = ( '' !== $focus_keyword && ! $keyword_used ) 554 ? __( 'Keyword usage low', 'jcodex-ai-product-generator-for-woocommerce' ) 555 : __( 'Keyword usage', 'jcodex-ai-product-generator-for-woocommerce' ); 556 557 $images_label = 'weak' === $image_status 558 ? __( 'Only 1 product image', 'jcodex-ai-product-generator-for-woocommerce' ) 559 : __( 'Product images', 'jcodex-ai-product-generator-for-woocommerce' ); 560 561 $title_conv_label = 'weak' === $title_status 562 ? __( 'Weak product title', 'jcodex-ai-product-generator-for-woocommerce' ) 563 : __( 'Product title', 'jcodex-ai-product-generator-for-woocommerce' ); 564 565 return array( 566 'content' => array( 567 $item( __( 'Product title', 'jcodex-ai-product-generator-for-woocommerce' ), $title_status ), 568 $item( __( 'Short description', 'jcodex-ai-product-generator-for-woocommerce' ), '' === $short_desc ? 'missing' : 'ok' ), 569 $item( $desc_label, $desc_status ), 570 $item( __( 'Benefits', 'jcodex-ai-product-generator-for-woocommerce' ), $has_benefits ? 'ok' : 'missing' ), 571 $item( __( 'FAQ', 'jcodex-ai-product-generator-for-woocommerce' ), $has_faq ? 'ok' : 'missing' ), 572 ), 573 'seo' => array( 574 $item( __( 'SEO title', 'jcodex-ai-product-generator-for-woocommerce' ), '' === $seo['title'] ? 'missing' : 'ok' ), 575 $item( $meta_label, $meta_status ), 576 $item( __( 'Focus keyword', 'jcodex-ai-product-generator-for-woocommerce' ), '' === $focus_keyword ? 'missing' : 'ok', false ), 577 $item( $keyword_label, '' === $focus_keyword ? 'missing' : ( $keyword_used ? 'ok' : 'weak' ), false ), 578 ), 579 'images' => array( 580 $item( __( 'Featured image', 'jcodex-ai-product-generator-for-woocommerce' ), $featured_id ? 'ok' : 'missing', false ), 581 $item( __( 'Image alt text', 'jcodex-ai-product-generator-for-woocommerce' ), empty( $image_ids ) ? 'missing' : ( $missing_alt ? 'missing' : 'ok' ) ), 582 $item( $images_label, $image_status, false ), 583 ), 584 'conversion' => array( 585 $item( __( 'FAQ', 'jcodex-ai-product-generator-for-woocommerce' ), $has_faq ? 'ok' : 'missing' ), 586 $item( __( 'Clear benefits', 'jcodex-ai-product-generator-for-woocommerce' ), $has_benefits ? 'ok' : 'missing' ), 587 $item( $title_conv_label, $title_status ), 588 ), 589 ); 590 } 183 591 private function get_seo_meta( $product_id ) { 184 592 $title = ''; $description = ''; … … 198 606 } 199 607 private function get_overall_score( $categories ) { $scores = wp_list_pluck( $categories, 'score' ); return (int) round( array_sum( $scores ) / max( 1, count( $scores ) ) ); } 608 private function visible_categories( $categories ) { 609 $out = array(); 610 foreach ( array( 'content', 'seo', 'images', 'conversion' ) as $key ) { 611 if ( isset( $categories[ $key ] ) ) { 612 $out[ $key ] = $categories[ $key ]; 613 } 614 } 615 return $out; 616 } 200 617 private function get_priority_issues( $categories ) { $issues = array(); foreach ( $categories as $category ) foreach ( $category['issues'] as $issue ) $issues[] = $issue; return array_slice( $issues, 0, 6 ); } 201 618 private function get_quick_wins( $categories ) { $wins = array(); foreach ( $categories as $category ) foreach ( $category['issues'] as $issue ) if ( ! empty( $issue['target'] ) ) $wins[] = $issue; return array_slice( $wins, 0, 4 ); } 619 620 /** 621 * UTF-8 character count for scoring thresholds. 622 * 623 * @param string $text Text. 624 * @return int 625 */ 626 private function text_len( $text ) { 627 if ( class_exists( 'Jcodex_Apgwco_Language' ) ) { 628 return Jcodex_Apgwco_Language::text_len( $text ); 629 } 630 return strlen( (string) $text ); 631 } 202 632 } 203 633 } -
jcodex-ai-product-generator-for-woocommerce/trunk/includes/class-remote.php
r3724671 r3732385 23 23 const OPT_USAGE = 'jcodex_apgwco_usage'; 24 24 const OPT_SKEW = 'jcodex_apgwco_time_skew'; 25 const OPT_INSTALL = 'jcodex_apgwco_install_id'; 26 const OPT_USAGE_REQ = 'jcodex_apgwco_usage_req'; 25 27 const T_CHALLENGE = 'jcodex_apgwco_reg_challenge'; 26 28 const T_REG_FAIL = 'jcodex_apgwco_reg_fail'; 29 const T_REG_LOCK = 'jcodex_apgwco_reg_lock'; 27 30 const T_LEGACY = 'jcodex_apgwco_legacy_mode'; 28 const T_USAGE_SYNC = 'jcodex_apgwco_usage_sync'; 31 const T_USAGE_SYNC = 'jcodex_apgwco_usage_sync_v2'; 32 33 /** 34 * Usage snapshot from the in-flight API response (this PHP request only). 35 * 36 * @var array<string,array> 37 */ 38 private static $last_usage = array(); 39 40 /** 41 * Types whose remaining was already dropped in this request. 42 * 43 * @var array<string,bool> 44 */ 45 private static $decremented = array(); 46 47 /** 48 * Whether the last generate-text/image reply said this request was charged. 49 * 50 * @var bool 51 */ 52 private static $last_charged = false; 29 53 30 54 public static function init() { … … 71 95 } 72 96 97 /** 98 * Stable ID for this WordPress install. Localhost cannot be verified 99 * from the API server, so this keeps each local site's quota separate. 100 */ 101 private static function install_id() { 102 $id = get_option( self::OPT_INSTALL, '' ); 103 if ( is_string( $id ) && 32 === strlen( $id ) && ctype_xdigit( $id ) ) { 104 return strtolower( $id ); 105 } 106 $id = bin2hex( random_bytes( 16 ) ); 107 update_option( self::OPT_INSTALL, $id, false ); 108 return $id; 109 } 110 73 111 /* -------------------------------------------------------------- 74 112 * Registration … … 77 115 $creds = get_option( self::OPT_CREDS ); 78 116 if ( is_array( $creds ) && ! empty( $creds['site_key'] ) && ! empty( $creds['secret'] ) && isset( $creds['host'] ) && self::current_host() === $creds['host'] ) { 117 if ( empty( $creds['install_bound'] ) ) { 118 delete_option( self::OPT_CREDS ); 119 delete_option( self::OPT_USAGE ); 120 return self::register(); 121 } 79 122 return $creds; 80 123 } … … 89 132 return false; 90 133 } 134 135 if ( get_transient( self::T_REG_LOCK ) ) { 136 $waited = self::wait_for_stored_creds( 22 ); 137 if ( $waited ) { 138 return $waited; 139 } 140 } 141 142 set_transient( self::T_REG_LOCK, 1, 30 ); 91 143 92 144 $challenge = bin2hex( random_bytes( 16 ) ); … … 98 150 'headers' => array( 'Content-Type' => 'application/json' ), 99 151 'body' => wp_json_encode( array( 100 'site_url' => get_site_url(), 101 'challenge' => $challenge, 102 'version' => defined( 'JCODEX_APGWCO_VERSION' ) ? JCODEX_APGWCO_VERSION : '', 152 'site_url' => get_site_url(), 153 'challenge' => $challenge, 154 'install_id' => self::install_id(), 155 'version' => defined( 'JCODEX_APGWCO_VERSION' ) ? JCODEX_APGWCO_VERSION : '', 103 156 ) ), 104 157 ) ); … … 107 160 108 161 if ( is_wp_error( $response ) ) { 162 delete_transient( self::T_REG_LOCK ); 109 163 set_transient( self::T_REG_FAIL, 1, MINUTE_IN_SECONDS ); 110 164 return false; … … 116 170 if ( 404 === $code ) { 117 171 // Service has not been upgraded yet: keep working in legacy (unsigned) mode for a while. 172 delete_transient( self::T_REG_LOCK ); 118 173 set_transient( self::T_LEGACY, 1, HOUR_IN_SECONDS ); 119 174 set_transient( self::T_REG_FAIL, 1, 10 * MINUTE_IN_SECONDS ); … … 122 177 123 178 if ( 200 !== $code || ! is_array( $data ) || empty( $data['site_key'] ) || empty( $data['secret'] ) ) { 179 delete_transient( self::T_REG_LOCK ); 124 180 set_transient( self::T_REG_FAIL, 1, MINUTE_IN_SECONDS ); 125 181 return false; … … 131 187 132 188 $creds = array( 133 'site_key' => sanitize_text_field( $data['site_key'] ), 134 'secret' => sanitize_text_field( $data['secret'] ), 135 'host' => self::current_host(), 136 'verified' => ! empty( $data['verified'] ), 189 'site_key' => sanitize_text_field( $data['site_key'] ), 190 'secret' => sanitize_text_field( $data['secret'] ), 191 'host' => self::current_host(), 192 'verified' => ! empty( $data['verified'] ), 193 'install_bound' => true, 137 194 ); 195 $previous = get_option( self::OPT_CREDS ); 196 $same_key = is_array( $previous ) && ! empty( $previous['site_key'] ) && $previous['site_key'] === $creds['site_key']; 197 if ( ! $same_key ) { 198 delete_option( self::OPT_USAGE ); 199 } 138 200 update_option( self::OPT_CREDS, $creds, false ); 201 delete_transient( self::T_REG_LOCK ); 139 202 return $creds; 203 } 204 205 /** 206 * Another request is already registering. Wait for stored creds instead of a second /register. 207 * 208 * @param int $seconds Max seconds to wait. 209 * @return array|false 210 */ 211 private static function wait_for_stored_creds( $seconds ) { 212 $seconds = max( 1, (int) $seconds ); 213 for ( $i = 0; $i < $seconds; $i++ ) { 214 $creds = self::stored_creds(); 215 if ( $creds ) { 216 return $creds; 217 } 218 if ( ! get_transient( self::T_REG_LOCK ) ) { 219 break; 220 } 221 sleep( 1 ); 222 } 223 return self::stored_creds(); 140 224 } 141 225 … … 146 230 * @param array $data payload for the endpoint 147 231 * @param string $type usage bucket (text, short_desc, image, faq ...) 148 * @param int $timeout seconds 232 * @param int $timeout seconds 233 * @param bool $allow_register Whether this call may register the site. 234 * @param bool $expect_charge False for a plugin-side free regenerate. 149 235 * @return array decoded response, or array( 'error' => msg, 'code' => code ) 150 236 * ------------------------------------------------------------ */ 151 public static function request( $endpoint, $data, $type = 'text', $timeout = 60 ) {152 $result = self::send( $endpoint, $data, $type, $timeout );237 public static function request( $endpoint, $data, $type = 'text', $timeout = 60, $allow_register = true, $expect_charge = true ) { 238 $result = self::send( $endpoint, $data, $type, $timeout, $allow_register, $expect_charge ); 153 239 154 240 // Self-heal: stale credentials or clock skew -> fix once and retry. … … 157 243 delete_option( self::OPT_CREDS ); 158 244 delete_transient( self::T_REG_FAIL ); 159 $result = self::send( $endpoint, $data, $type, $timeout );245 $result = self::send( $endpoint, $data, $type, $timeout, $allow_register, $expect_charge ); 160 246 } elseif ( 'bad_timestamp' === $result['code'] && ! empty( $result['server_time'] ) ) { 161 247 update_option( self::OPT_SKEW, (int) $result['server_time'] - time(), false ); 162 $result = self::send( $endpoint, $data, $type, $timeout ); 248 $result = self::send( $endpoint, $data, $type, $timeout, $allow_register, $expect_charge ); 249 } elseif ( 'replay' === $result['code'] ) { 250 $result = self::send( $endpoint, $data, $type, $timeout, $allow_register, $expect_charge ); 163 251 } 164 252 } … … 167 255 } 168 256 169 private static function send( $endpoint, $data, $type, $timeout ) { 257 /** 258 * Credentials already stored for this host. Does not call register(). 259 * 260 * @return array|false 261 */ 262 private static function stored_creds() { 263 $creds = get_option( self::OPT_CREDS ); 264 if ( is_array( $creds ) && ! empty( $creds['site_key'] ) && ! empty( $creds['secret'] ) && isset( $creds['host'] ) && self::current_host() === $creds['host'] ) { 265 return $creds; 266 } 267 return false; 268 } 269 270 public static function last_was_charged() { 271 return self::$last_charged; 272 } 273 274 private static function send( $endpoint, $data, $type, $timeout, $allow_register = true, $expect_charge = true ) { 275 $started = microtime( true ); 170 276 $payload = array( 171 277 'site_url' => get_site_url(), 172 278 'type' => $type, 173 279 'data' => $data, 280 'nonce' => bin2hex( random_bytes( 8 ) ), 174 281 ); 175 282 $license = self::license_key(); … … 179 286 180 287 $headers = array( 'Content-Type' => 'application/json' ); 181 $creds = get_transient( self::T_LEGACY ) ? false : self::get_creds(); 182 $body = wp_json_encode( $payload ); 288 $legacy = (bool) get_transient( self::T_LEGACY ); 289 if ( $legacy ) { 290 $creds = false; 291 } elseif ( $allow_register ) { 292 $creds = self::get_creds(); 293 } else { 294 $creds = self::stored_creds(); 295 } 296 297 if ( ! $creds && ! $legacy ) { 298 return array( 299 'error' => __( 'Could not register this site with the AI service. Please try again in a minute.', 'jcodex-ai-product-generator-for-woocommerce' ), 300 'code' => 'registration_failed', 301 ); 302 } 303 304 $body = wp_json_encode( $payload ); 183 305 184 306 if ( $creds ) { 185 $ts = self::time_now();307 $ts = self::time_now(); 186 308 $headers['X-APG-Key'] = $creds['site_key']; 187 309 $headers['X-APG-Time'] = (string) $ts; … … 197 319 198 320 if ( is_wp_error( $response ) ) { 199 return array( 'error' => esc_html( $response->get_error_message()), 'code' => 'http_error_local' );321 return array( 'error' => $response->get_error_message(), 'code' => 'http_error_local' ); 200 322 } 201 323 … … 203 325 $decoded = json_decode( wp_remote_retrieve_body( $response ), true ); 204 326 327 $expect_charge = (bool) $expect_charge; 328 $before = self::cached_remaining( $type ); 329 $has_charge_flag = is_array( $decoded ) && array_key_exists( 'charged', $decoded ); 330 $charged = $has_charge_flag ? (bool) $decoded['charged'] : false; 331 self::$last_charged = $charged; 332 205 333 if ( is_array( $decoded ) && ! empty( $decoded['usage'] ) && is_array( $decoded['usage'] ) ) { 206 self::store_usage( $decoded['usage'] ); 334 self::$last_usage = $decoded['usage']; 335 self::store_usage( $decoded['usage'], $started ); 336 if ( $has_charge_flag && ( ! $expect_charge || ! $charged ) ) { 337 self::restore_type_remaining( $type, $before ); 338 } elseif ( $has_charge_flag && $charged && ! self::is_pro_active() ) { 339 $after = self::cached_remaining( $type ); 340 if ( $after < $before ) { 341 self::$decremented[ sanitize_key( $type ) ] = true; 342 } else { 343 self::ensure_type_decremented( $type ); 344 } 345 } 207 346 } 208 347 … … 215 354 $status 216 355 ); 217 $out = array( 'error' => esc_html( $msg ), 'code' => ( is_array( $decoded ) && ! empty( $decoded['code'] ) ) ? (string) $decoded['code'] : 'http_' . $status );356 $out = array( 'error' => $msg, 'code' => ( is_array( $decoded ) && ! empty( $decoded['code'] ) ) ? (string) $decoded['code'] : 'http_' . $status ); 218 357 if ( is_array( $decoded ) && ! empty( $decoded['server_time'] ) ) { 219 358 $out['server_time'] = (int) $decoded['server_time']; … … 223 362 224 363 if ( ! is_array( $decoded ) ) { 225 return array( 'error' => esc_html__( 'Invalid JSON returned from API.', 'jcodex-ai-product-generator-for-woocommerce' ), 'code' => 'bad_json' );364 return array( 'error' => __( 'Invalid JSON returned from API.', 'jcodex-ai-product-generator-for-woocommerce' ), 'code' => 'bad_json' ); 226 365 } 227 366 … … 232 371 * Usage cache (display + UX only - the server is the source of truth) 233 372 * ------------------------------------------------------------ */ 234 private static function store_usage( $usage ) { 235 $clean = array(); 373 /** 374 * @param array $row Raw usage row. 375 * @return array{remaining:int,limit:int,reset_at:int}|null 376 */ 377 private static function normalize_usage_row( $row ) { 378 if ( ! is_array( $row ) || ! isset( $row['remaining'], $row['limit'] ) ) { 379 return null; 380 } 381 return array( 382 'remaining' => (int) $row['remaining'], 383 'limit' => (int) $row['limit'], 384 'reset_at' => isset( $row['reset_at'] ) ? (int) $row['reset_at'] : 0, 385 ); 386 } 387 388 /** 389 * Merge a server snapshot into the local cache. Remaining for the same 390 * period is never allowed to go up — that is how a late /usage reply 391 * was putting badges back to 10 after generate had already charged. 392 */ 393 private static function store_usage( $usage, $request_started = 0.0 ) { 394 $existing = get_option( self::OPT_USAGE ); 395 if ( ! is_array( $existing ) ) { 396 $existing = array(); 397 } 398 399 $changed = false; 236 400 foreach ( $usage as $type => $row ) { 237 if ( is_array( $row ) && isset( $row['remaining'], $row['limit'] ) ) { 238 $clean[ sanitize_key( $type ) ] = array( 239 'remaining' => (int) $row['remaining'], 240 'limit' => (int) $row['limit'], 241 'reset_at' => isset( $row['reset_at'] ) ? (int) $row['reset_at'] : 0, 242 ); 243 } 244 } 245 if ( $clean ) { 246 update_option( self::OPT_USAGE, $clean, false ); 401 $incoming = self::normalize_usage_row( $row ); 402 if ( ! $incoming ) { 403 continue; 404 } 405 $type = sanitize_key( $type ); 406 if ( isset( $existing[ $type ] ) ) { 407 $old = self::normalize_usage_row( $existing[ $type ] ); 408 if ( $old ) { 409 if ( $incoming['reset_at'] < $old['reset_at'] ) { 410 continue; 411 } 412 if ( $incoming['reset_at'] === $old['reset_at'] && $incoming['remaining'] > $old['remaining'] ) { 413 continue; 414 } 415 } 416 } 417 if ( ! isset( $existing[ $type ] ) || $existing[ $type ] !== $incoming ) { 418 $existing[ $type ] = $incoming; 419 $changed = true; 420 } 421 } 422 423 if ( $changed ) { 424 update_option( self::OPT_USAGE, $existing, false ); 425 } 426 427 $request_started = (float) $request_started; 428 $last_started = (float) get_option( self::OPT_USAGE_REQ, 0 ); 429 if ( $request_started > 0 && $request_started >= $last_started ) { 430 update_option( self::OPT_USAGE_REQ, (string) $request_started, false ); 247 431 } 248 432 } … … 252 436 */ 253 437 public static function get_usage( $type ) { 438 $row = null; 439 if ( isset( self::$last_usage[ $type ] ) ) { 440 $row = self::normalize_usage_row( self::$last_usage[ $type ] ); 441 } 442 if ( ! $row ) { 443 $all = get_option( self::OPT_USAGE ); 444 if ( is_array( $all ) && isset( $all[ $type ] ) ) { 445 $row = self::normalize_usage_row( $all[ $type ] ); 446 } 447 } 448 if ( ! $row ) { 449 return null; 450 } 451 // If the period already ended, the server will have reset it. 452 if ( $row['reset_at'] > 0 && $row['reset_at'] <= time() ) { 453 return array( 'remaining' => $row['limit'], 'limit' => $row['limit'], 'reset_at' => 0 ); 454 } 455 return $row; 456 } 457 458 /** 459 * Remaining for a type from this response, then the option cache, else 10. 460 * 461 * @param string $type Usage bucket. 462 * @return int 463 */ 464 private static function cached_remaining( $type ) { 465 $row = self::get_usage( $type ); 466 return $row ? max( 0, (int) $row['remaining'] ) : 10; 467 } 468 469 /** 470 * Put this type's remaining back after a free regenerate so the badge 471 * cannot fall when the success text says no credit was used. 472 * 473 * @param string $type Usage bucket. 474 * @param int $remaining Value from before this request. 475 */ 476 private static function restore_type_remaining( $type, $remaining ) { 477 $type = sanitize_key( $type ); 478 if ( '' === $type ) { 479 return; 480 } 481 $row = self::get_usage( $type ); 482 $updated = array( 483 'remaining' => max( 0, (int) $remaining ), 484 'limit' => $row ? (int) $row['limit'] : 10, 485 'reset_at' => $row ? (int) $row['reset_at'] : 0, 486 ); 487 self::$last_usage[ $type ] = $updated; 254 488 $all = get_option( self::OPT_USAGE ); 255 if ( is_array( $all ) && isset( $all[ $type ] ) ) { 256 $row = $all[ $type ]; 257 // If the period already ended, the server will have reset it. 258 if ( $row['reset_at'] > 0 && $row['reset_at'] <= time() ) { 259 return array( 'remaining' => $row['limit'], 'limit' => $row['limit'], 'reset_at' => 0 ); 260 } 261 return $row; 262 } 263 return null; 489 if ( ! is_array( $all ) ) { 490 $all = array(); 491 } 492 $all[ $type ] = $updated; 493 update_option( self::OPT_USAGE, $all, false ); 494 } 495 496 /** 497 * Drop one unit on the local display cache when the server charged but 498 * the snapshot for this tab type did not change (legacy_text pool, etc.). 499 * 500 * @param string $type Usage bucket. 501 */ 502 public static function ensure_type_decremented( $type ) { 503 $type = sanitize_key( $type ); 504 if ( '' === $type || ! empty( self::$decremented[ $type ] ) ) { 505 return; 506 } 507 $row = self::get_usage( $type ); 508 $limit = $row ? (int) $row['limit'] : 10; 509 $reset = $row ? (int) $row['reset_at'] : 0; 510 $remaining = $row ? (int) $row['remaining'] : 10; 511 $updated = array( 512 'remaining' => max( 0, $remaining - 1 ), 513 'limit' => $limit, 514 'reset_at' => $reset, 515 ); 516 self::$last_usage[ $type ] = $updated; 517 self::$decremented[ $type ] = true; 518 $all = get_option( self::OPT_USAGE ); 519 if ( ! is_array( $all ) ) { 520 $all = array(); 521 } 522 $all[ $type ] = $updated; 523 update_option( self::OPT_USAGE, $all, false ); 524 } 525 526 /** 527 * True when this site already has a usage snapshot from the server. 528 * 529 * @return bool 530 */ 531 public static function has_usage_cache() { 532 $all = get_option( self::OPT_USAGE ); 533 return is_array( $all ) && ! empty( $all ); 264 534 } 265 535 266 536 /** 267 537 * Pull fresh numbers from the server (at most once per 6 hours, short timeout). 538 * Never registers the site from a product-editor page render. 268 539 */ 269 540 public static function maybe_sync_usage() { … … 271 542 return; 272 543 } 544 if ( get_transient( self::T_REG_FAIL ) ) { 545 return; 546 } 547 if ( ! get_transient( self::T_LEGACY ) && ! self::stored_creds() ) { 548 return; 549 } 273 550 set_transient( self::T_USAGE_SYNC, 1, 6 * HOUR_IN_SECONDS ); 274 self::request( '/usage', new stdClass(), 'text', 4 ); 551 self::request( '/usage', new stdClass(), 'text', 4, false ); 552 } 553 554 /** 555 * Fetch usage for the editor badges. Safe to call from AJAX; may register the site. 556 * 557 * @return bool 558 */ 559 public static function sync_usage() { 560 if ( self::is_pro_active() ) { 561 return true; 562 } 563 $result = self::request( '/usage', new stdClass(), 'text', 15, true ); 564 if ( ! empty( $result['error'] ) ) { 565 return false; 566 } 567 set_transient( self::T_USAGE_SYNC, 1, 6 * HOUR_IN_SECONDS ); 568 return true; 275 569 } 276 570 } -
jcodex-ai-product-generator-for-woocommerce/trunk/readme.txt
r3724671 r3732385 3 3 Tags: woocommerce ai, ai product description, ai image generator, product content, seo 4 4 Requires at least: 5.8 5 Tested up to: 7.1. 25 Tested up to: 7.1.3 6 6 Requires PHP: 7.4 7 Stable tag: 1.2. 57 Stable tag: 1.2.6 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)