Changeset 3798396
- Timestamp:
- 11/12/2026 01:24:47 PM (less than one hour ago)
- Location:
- smashballoon-wpchat-livechat-customer-support/trunk
- Files:
-
- 2 added
- 11 edited
-
readme.txt (modified) (2 diffs)
-
src/Common/RestAPI/SettingsEndpoint.php (modified) (5 diffs)
-
src/Common/RestAPI/SmartSearchEndpoint.php (modified) (1 diff)
-
src/Common/Services/AdminPagesService.php (modified) (16 diffs)
-
src/Common/Services/Database/FaqSearchService.php (modified) (3 diffs)
-
src/Common/Services/SettingsService.php (modified) (3 diffs)
-
src/Common/Services/SingletonContainer.php (modified) (1 diff)
-
src/Common/Services/SmartSearchManagerService.php (modified) (1 diff)
-
src/Common/Services/SmartSearchRateLimiter.php (added)
-
src/Common/Services/VectorEmbeddingService.php (modified) (2 diffs)
-
templates/admin-connect-token.php (added)
-
vendor/composer/installed.php (modified) (1 diff)
-
wp-chat.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
smashballoon-wpchat-livechat-customer-support/trunk/readme.txt
r3760121 r3798396 7 7 Tested up to: 7.1 8 8 Requires PHP: 8.0 9 Stable tag: 1.4. 59 Stable tag: 1.4.6 10 10 License: GPLv2 or later 11 11 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 257 257 == Changelog == 258 258 259 = 1.4.6 = 260 * Tweak: Connecting Smart Search from the emailed link now opens a "Connect Smart Search" screen in WPChat, so you confirm before Smart Search connects. 261 * Tweak: Smart Search now limits how many FAQ searches one visitor can make, so a single visitor can't use up your site's Smart Search tokens. Once a limit is reached, search falls back to keyword matching. 262 * Fix: The Smart Search email-verification window now lasts a full 24 hours on sites not set to UTC. 263 * Fix: Plugin security hardening. 264 259 265 = 1.4.5 = 260 266 * New: Added a "Visitor interaction analytics" setting in WPChat > Settings to turn chat analytics off, plus the `wpchat_analytics_enabled` filter so consent plugins can turn analytics off per visitor, and the `wpchat_smart_search_enabled` filter to keep FAQ search on your own site. -
smashballoon-wpchat-livechat-customer-support/trunk/src/Common/RestAPI/SettingsEndpoint.php
r3760121 r3798396 5 5 use SmashBalloon\WPChat\Common\Services\SettingsService; 6 6 use SmashBalloon\WPChat\Common\Services\PrivateSettingsService; 7 use SmashBalloon\WPChat\Common\Services\SmartSearchManagerService; 7 8 use SmashBalloon\WPChat\Common\Services\ApiService; 8 9 use WP_REST_Request; … … 39 40 40 41 /** 42 * The Smart Search manager, which decides whether a claim is recent. 43 * 44 * @var SmartSearchManagerService 45 */ 46 private $smartSearchManager; 47 48 /** 41 49 * Constructor. 42 50 * 43 * @param SettingsService $settingsService The settings service. 44 * @param PrivateSettingsService $privateSettingsService The private settings service. 45 */ 46 public function __construct(SettingsService $settingsService, PrivateSettingsService $privateSettingsService) { 51 * @param SettingsService $settingsService The settings service. 52 * @param PrivateSettingsService $privateSettingsService The private settings service. 53 * @param SmartSearchManagerService $smartSearchManager The Smart Search manager. 54 */ 55 public function __construct( 56 SettingsService $settingsService, 57 PrivateSettingsService $privateSettingsService, 58 SmartSearchManagerService $smartSearchManager 59 ) { 47 60 $this->settingsService = $settingsService; 48 61 $this->privateSettingsService = $privateSettingsService; 62 $this->smartSearchManager = $smartSearchManager; 49 63 } 50 64 … … 95 109 * Get all settings. 96 110 * 111 * The route is public because the front-end widget reads it. The Smart Search 112 * account fields (token state, usage and the newsletter email that identifies the 113 * site to the WPChat API) come from private settings and go to admins only. 114 * 97 115 * @return WP_REST_Response 98 116 */ 99 117 public function getSettings() { 100 118 $settings = $this->settingsService->getAllSettings(); 119 120 if (!$this->checkPermission()) { 121 return rest_ensure_response($settings); 122 } 101 123 102 124 // Add a flag to indicate if an access token exists … … 370 392 private function sanitizeSettingsFromRequest(array $settingsData): array { 371 393 $sanitizedSettings = array(); 394 $settingsData = array_diff_key( $settingsData, array_flip( SettingsService::ADMIN_ONLY_KEYS ) ); 372 395 373 396 foreach ( $settingsData as $key => $value ) { … … 481 504 */ 482 505 private function isNewsletterWithin24Hours(): bool { 483 $isSubscribed = (bool)$this->privateSettingsService->getSetting( 'newsletter_subscribed', false ); 484 $subscriptionDate = $this->privateSettingsService->getSetting( 'newsletter_subscription_date', '' ); 485 486 if ( ! $isSubscribed || empty( $subscriptionDate ) ) { 487 return false; 488 } 489 490 try { 491 $subscriptionTime = new \DateTime( $subscriptionDate ); 492 $now = new \DateTime(); 493 $hoursDiff = ($now->getTimestamp() - $subscriptionTime->getTimestamp()) / 3600; 494 495 return $hoursDiff < 24; 496 } catch ( \Exception $e ) { 497 return false; 498 } 506 // The same check as the Connect screen: the claim date is stored in the site's 507 // timezone, so reading it as UTC is off by the site's offset (SMASH-2676). 508 return $this->smartSearchManager->hasRecentClaimOffer(); 499 509 } 500 510 } -
smashballoon-wpchat-livechat-customer-support/trunk/src/Common/RestAPI/SmartSearchEndpoint.php
r3376414 r3798396 52 52 'methods' => \WP_REST_Server::CREATABLE, // POST 53 53 'callback' => array($this, 'claimOffer'), 54 'permission_callback' => array($this, 'checkPublicPermission'), 54 // Admin only: the claim registers the site with the WPChat API and 55 // rewrites the site's private settings, so the public widget nonce is not enough. 56 'permission_callback' => array($this, 'checkPermission'), 55 57 'args' => array( 56 58 'email' => array( -
smashballoon-wpchat-livechat-customer-support/trunk/src/Common/Services/AdminPagesService.php
r3584324 r3798396 4 4 5 5 if ( ! defined( 'ABSPATH' ) ) { 6 exit;6 exit; 7 7 } 8 8 … … 23 23 * @var SettingsService 24 24 */ 25 private $settingsService;25 private $settingsService; 26 26 27 27 /** … … 30 30 * @var PrivateSettingsService 31 31 */ 32 private $privateSettingsService;32 private $privateSettingsService; 33 33 34 34 /** … … 37 37 * @var GateInterface 38 38 */ 39 private GateInterface $gate; 40 41 /** 42 * Constructor. 43 * 44 * @param SettingsService $settingsService The settings service. 45 * @param PrivateSettingsService $privateSettingsService The private settings service. 46 * @param GateInterface $gate The gate service. 47 */ 48 public function __construct( SettingsService $settingsService, PrivateSettingsService $privateSettingsService, GateInterface $gate ) { 49 $this->settingsService = $settingsService; 50 $this->privateSettingsService = $privateSettingsService; 51 $this->gate = $gate; 52 } 39 private GateInterface $gate; 40 41 /** 42 * Nonce action for saving the access token from the verification email. 43 */ 44 public const CONNECT_TOKEN_NONCE_ACTION = 'wpchat_connect_access_token'; 45 46 /** 47 * Per-user transient holding a token from the verification link until the admin confirms it. 48 */ 49 public const PENDING_TOKEN_TRANSIENT = 'wpchat_pending_api_token_'; 50 51 /** 52 * Smart Search manager, for the record of this site's own claim. 53 * 54 * @var SmartSearchManagerService 55 */ 56 private SmartSearchManagerService $smartSearchManager; 57 58 /** 59 * Constructor. 60 * 61 * @param SettingsService $settingsService The settings service. 62 * @param PrivateSettingsService $privateSettingsService The private settings service. 63 * @param GateInterface $gate The gate service. 64 * @param SmartSearchManagerService $smartSearchManager The Smart Search manager. 65 */ 66 public function __construct( 67 SettingsService $settingsService, 68 PrivateSettingsService $privateSettingsService, 69 GateInterface $gate, 70 SmartSearchManagerService $smartSearchManager 71 ) { 72 $this->settingsService = $settingsService; 73 $this->privateSettingsService = $privateSettingsService; 74 $this->gate = $gate; 75 $this->smartSearchManager = $smartSearchManager; 76 } 53 77 54 78 /** … … 57 81 * @inheritDoc 58 82 */ 59 public function register(): void { 60 add_action( 'admin_menu', array($this, 'addAdminMenu') ); 61 add_action( 'admin_init', array($this, 'handleAccessToken') ); 62 add_action( 'in_admin_header', array($this, 'removeAdminNotices') ); 63 add_filter( 'user_has_cap', array($this, 'filterUserCapabilities'), 10, 4 ); 64 add_filter( 'members_get_capabilities', array($this, 'registerMembersCapabilities') ); 65 } 83 public function register(): void { 84 add_action( 'admin_menu', array($this, 'addAdminMenu') ); 85 add_action( 'admin_init', array($this, 'handleAccessToken') ); 86 add_action('load-toplevel_page_wp-chat', array($this, 'captureAccessTokenFromUrl')); 87 add_action( 'in_admin_header', array($this, 'removeAdminNotices') ); 88 add_filter( 'user_has_cap', array($this, 'filterUserCapabilities'), 10, 4 ); 89 add_filter( 'members_get_capabilities', array($this, 'registerMembersCapabilities') ); 90 } 66 91 67 92 /** … … 70 95 * @return void 71 96 **/ 72 public function addAdminMenu() {73 $icon_url = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjEwODQgMi4wMDUxOUMxMy4zNjIgMi4wMDUxOSAxNiA0LjY0MzIxIDE2IDcuODk2NzlDMTYgMTEuMjI3NiAxMy4yNjIyIDEzLjgzMyAxMC4wNDEgMTMuNzg5NEwwIDE0LjMyNTVMMS4xMzE4NCAxMC44ODMxQzAuNDE4NzkxIDkuOTQ4MDMgMCA4Ljc4Mjg1IDAgNy41MzI1M0M2LjU1NDE0ZS0wNSA0LjQ4MDEyIDIuNDc0OTIgMi4wMDUxOSA1LjUyNzM0IDIuMDA1MTlIMTAuMTA4NFpNNC4zODA4NiA3LjU5MzA4VjcuNjU2NTZDNC4zODA5MiA5LjQ3OTkxIDUuODU5MjUgMTAuOTU4MyA3LjY4MjYyIDEwLjk1ODNDOS41MDU5IDEwLjk1ODIgMTAuOTg0MyA5LjQ3OTg1IDEwLjk4NDQgNy42NTY1NlY3LjU5MzA4SDkuMzMzMDEgVjcuNjU2NTZDOS4zMzI5NSA4LjU2ODE0IDguNTk0MiA5LjMwNjg0IDcuNjgyNjIgOS4zMDY5NUM2Ljc3MDk1IDkuMzA2OTUgNi4wMzEzMSA4LjU2ODIxIDYuMDMxMjUgNy42NTY1NlY3LjU5MzA4SDQuMzgwODZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4=';74 75 $icon_url = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjEwODQgMi4wMDUxOUMxMy4zNjIgMi4wMDUxOSAxNiA0LjY0MzIxIDE2IDcuODk2NzlDMTYgMTEuMjI3NiAxMy4yNjIyIDEzLjgzMyAxMC4wNDEgMTMuNzg5NEwwIDE0LjMyNTVMMS4xMzE4NCAxMC44ODMxQzAuNDE4NzkxIDkuOTQ4MDMgMCA4Ljc4Mjg1IDAgNy41MzI1M0M2LjU1NDE0ZS0wNSA0LjQ4MDEyIDIuNDc0OTIgMi4wMDUxOSA1LjUyNzM0IDIuMDA1MTlIMTAuMTA4NFpNNC4zODA4NiA3LjU5MzA4VjcuNjU2NTZDNC4zODA5MiA5LjQ3OTkxIDUuODU5MjUgMTAuOTU4MyA3LjY4MjYyIDEwLjk1ODNDOS41MDU5IDEwLjk1ODIgMTAuOTg0MyA5LjQ3OTg1IDEwLjk4NDQgNy42NTY1NlY3LjU5MzA4SDkuMzMzMDEgVjcuNjU2NTZDOS4zMzI5NSA4LjU2ODE0IDguNTk0MiA5LjMwNjg0IDcuNjgyNjIgOS4zMDY5NUM2Ljc3MDk1IDkuMzA2OTUgNi4wMzEzMSA4LjU2ODIxIDYuMDMxMjUgNy42NTY1NlY3LjU5MzA4SDQuMzgwODZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4=';76 77 add_menu_page(78 __( 'WPChat', 'smashballoon-wpchat-livechat-customer-support' ),79 __( 'WPChat', 'smashballoon-wpchat-livechat-customer-support' ),80 'wpchat_admin',81 'wp-chat',82 array($this, 'renderAdminPage'),83 $icon_url,84 2685 );86 87 // Add Overview as first submenu item88 add_submenu_page(89 'wp-chat',90 __( 'Overview', 'smashballoon-wpchat-livechat-customer-support' ),91 __( 'Overview', 'smashballoon-wpchat-livechat-customer-support' ),92 'wpchat_admin',93 'wp-chat',94 array($this, 'renderAdminPage'),95 );96 97 $settings = $this->settingsService->getAllSettings();98 $onboardingStatus = $settings['onboardingStatus'] ?? false;99 100 if ( $onboardingStatus ) {101 add_submenu_page(97 public function addAdminMenu() { 98 $icon_url = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjEwODQgMi4wMDUxOUMxMy4zNjIgMi4wMDUxOSAxNiA0LjY0MzIxIDE2IDcuODk2NzlDMTYgMTEuMjI3NiAxMy4yNjIyIDEzLjgzMyAxMC4wNDEgMTMuNzg5NEwwIDE0LjMyNTVMMS4xMzE4NCAxMC44ODMxQzAuNDE4NzkxIDkuOTQ4MDMgMCA4Ljc4Mjg1IDAgNy41MzI1M0M2LjU1NDE0ZS0wNSA0LjQ4MDEyIDIuNDc0OTIgMi4wMDUxOSA1LjUyNzM0IDIuMDA1MTlIMTAuMTA4NFpNNC4zODA4NiA3LjU5MzA4VjcuNjU2NTZDNC4zODA5MiA5LjQ3OTkxIDUuODU5MjUgMTAuOTU4MyA3LjY4MjYyIDEwLjk1ODNDOS41MDU5IDEwLjk1ODIgMTAuOTg0MyA5LjQ3OTg1IDEwLjk4NDQgNy42NTY1NlY3LjU5MzA4SDkuMzMzMDEgVjcuNjU2NTZDOS4zMzI5NSA4LjU2ODE0IDguNTk0MiA5LjMwNjg0IDcuNjgyNjIgOS4zMDY5NUM2Ljc3MDk1IDkuMzA2OTUgNi4wMzEzMSA4LjU2ODIxIDYuMDMxMjUgNy42NTY1NlY3LjU5MzA4SDQuMzgwODZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4='; 99 100 $icon_url = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEwLjEwODQgMi4wMDUxOUMxMy4zNjIgMi4wMDUxOSAxNiA0LjY0MzIxIDE2IDcuODk2NzlDMTYgMTEuMjI3NiAxMy4yNjIyIDEzLjgzMyAxMC4wNDEgMTMuNzg5NEwwIDE0LjMyNTVMMS4xMzE4NCAxMC44ODMxQzAuNDE4NzkxIDkuOTQ4MDMgMCA4Ljc4Mjg1IDAgNy41MzI1M0M2LjU1NDE0ZS0wNSA0LjQ4MDEyIDIuNDc0OTIgMi4wMDUxOSA1LjUyNzM0IDIuMDA1MTlIMTAuMTA4NFpNNC4zODA4NiA3LjU5MzA4VjcuNjU2NTZDNC4zODA5MiA5LjQ3OTkxIDUuODU5MjUgMTAuOTU4MyA3LjY4MjYyIDEwLjk1ODNDOS41MDU5IDEwLjk1ODIgMTAuOTg0MyA5LjQ3OTg1IDEwLjk4NDQgNy42NTY1NlY3LjU5MzA4SDkuMzMzMDEgVjcuNjU2NTZDOS4zMzI5NSA4LjU2ODE0IDguNTk0MiA5LjMwNjg0IDcuNjgyNjIgOS4zMDY5NUM2Ljc3MDk1IDkuMzA2OTUgNi4wMzEzMSA4LjU2ODIxIDYuMDMxMjUgNy42NTY1NlY3LjU5MzA4SDQuMzgwODZaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4='; 101 102 add_menu_page( 103 __( 'WPChat', 'smashballoon-wpchat-livechat-customer-support' ), 104 __( 'WPChat', 'smashballoon-wpchat-livechat-customer-support' ), 105 'wpchat_admin', 106 'wp-chat', 107 array($this, 'renderAdminPage'), 108 $icon_url, 109 26 110 ); 111 112 // Add Overview as first submenu item 113 add_submenu_page( 114 'wp-chat', 115 __( 'Overview', 'smashballoon-wpchat-livechat-customer-support' ), 116 __( 'Overview', 'smashballoon-wpchat-livechat-customer-support' ), 117 'wpchat_admin', 118 'wp-chat', 119 array($this, 'renderAdminPage'), 120 ); 121 122 $settings = $this->settingsService->getAllSettings(); 123 $onboardingStatus = $settings['onboardingStatus'] ?? false; 124 125 if ( $onboardingStatus ) { 126 add_submenu_page( 102 127 'wp-chat', 103 128 __( 'Visibility', 'smashballoon-wpchat-livechat-customer-support' ), … … 106 131 'wp-chat#/visibility', 107 132 array($this, 'renderAdminPage'), 108 );109 110 add_submenu_page(133 ); 134 135 add_submenu_page( 111 136 'wp-chat', 112 137 __( 'Customizer', 'smashballoon-wpchat-livechat-customer-support' ), … … 115 140 'wp-chat#/customizer', 116 141 array($this, 'renderAdminPage'), 117 );118 119 120 add_submenu_page(142 ); 143 144 145 add_submenu_page( 121 146 'wp-chat', 122 147 __( 'Agents', 'smashballoon-wpchat-livechat-customer-support' ), … … 125 150 'wp-chat#/agents', 126 151 array($this, 'renderAdminPage'), 127 );128 129 add_submenu_page(152 ); 153 154 add_submenu_page( 130 155 'wp-chat', 131 156 __( 'Frequent Questions', 'smashballoon-wpchat-livechat-customer-support' ), … … 134 159 'wp-chat#/faqs', 135 160 array($this, 'renderAdminPage'), 136 );137 138 add_submenu_page(161 ); 162 163 add_submenu_page( 139 164 'wp-chat', 140 165 __( 'Chat Funnels', 'smashballoon-wpchat-livechat-customer-support' ), … … 143 168 'wp-chat#/funnels', 144 169 array($this, 'renderAdminPage'), 145 );146 147 add_submenu_page(170 ); 171 172 add_submenu_page( 148 173 'wp-chat', 149 174 __( 'Settings', 'smashballoon-wpchat-livechat-customer-support' ), … … 152 177 'wp-chat#/settings', 153 178 array($this, 'renderAdminPage'), 154 );155 156 add_submenu_page(179 ); 180 181 add_submenu_page( 157 182 'wp-chat', 158 183 __( 'Support', 'smashballoon-wpchat-livechat-customer-support' ), … … 161 186 'wp-chat#/support', 162 187 array($this, 'renderAdminPage'), 163 );164 165 if ( ! defined( 'WPCHAT_PRO' ) || ! WPCHAT_PRO ) {166 add_submenu_page(167 'wp-chat',168 __( 'Upgrade', 'smashballoon-wpchat-livechat-customer-support' ),169 sprintf(170 '<div class="sb-wpchat-pro-upgrade-link-container"><div class="sb-wpchat-pro-upgrade-link-bg"></div><strong class="sb-wpchat-pro-upgrade-link">%s</strong></div>',171 __( 'Upgrade', 'smashballoon-wpchat-livechat-customer-support' )172 ),173 'wpchat_admin',174 UTMUrlGenerator::upgradeUrl(),175 ''176 );177 }178 }179 180 }188 ); 189 190 if ( ! defined( 'WPCHAT_PRO' ) || ! WPCHAT_PRO ) { 191 add_submenu_page( 192 'wp-chat', 193 __( 'Upgrade', 'smashballoon-wpchat-livechat-customer-support' ), 194 sprintf( 195 '<div class="sb-wpchat-pro-upgrade-link-container"><div class="sb-wpchat-pro-upgrade-link-bg"></div><strong class="sb-wpchat-pro-upgrade-link">%s</strong></div>', 196 __( 'Upgrade', 'smashballoon-wpchat-livechat-customer-support' ) 197 ), 198 'wpchat_admin', 199 UTMUrlGenerator::upgradeUrl(), 200 '' 201 ); 202 } 203 } 204 205 } 181 206 182 207 /** … … 185 210 * @return void 186 211 */ 187 public function renderAdminPage() { 188 include WPCHAT_PLUGIN_DIR . 'templates/admin-ui.php'; 189 } 190 191 /** 192 * Handle access token capture from URL parameters 212 public function renderAdminPage() 213 { 214 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Selects which screen to show; no write. 215 $connectState = isset($_GET['wpchat_connect']) ? sanitize_key(wp_unslash($_GET['wpchat_connect'])) : ''; 216 217 if (in_array($connectState, array('confirm', 'none'), true) && $this->currentUserIsWpChatAdmin()) { 218 $connectId = $this->getPending()['id']; 219 if ('confirm' === $connectState && '' === $connectId) { 220 $connectState = 'none'; 221 } 222 include WPCHAT_PLUGIN_DIR . 'templates/admin-connect-token.php'; 223 return; 224 } 225 226 include WPCHAT_PLUGIN_DIR . 'templates/admin-ui.php'; 227 } 228 229 /** 230 * Move the token from the verification link out of the URL, before the page renders. 231 * 232 * After email verification, the WPChat API sends the admin to admin.php?page=wp-chat&access_token=…. 233 * That URL cannot carry a nonce, so the token is only parked for the current admin, and only 234 * when this site claimed Smart Search in the last 24 hours (the life of the emailed link). 235 * handleAccessToken() saves it after the admin confirms (SMASH-2676). 236 * 237 * @return void 238 */ 239 public function captureAccessTokenFromUrl() 240 { 241 $token = $this->getAccessTokenFromUrl(); 242 if ('' === $token || !$this->currentUserIsWpChatAdmin()) { 243 return; 244 } 245 246 $state = 'none'; 247 if ($this->smartSearchManager->hasRecentClaimOffer()) { 248 // The id goes into the Connect form, so the POST saves only the token its screen was for. 249 $pending = array('token' => $token, 'id' => wp_generate_password(20, false, false)); 250 set_transient(self::PENDING_TOKEN_TRANSIENT . get_current_user_id(), $pending, 30 * MINUTE_IN_SECONDS); 251 $state = 'confirm'; 252 } 253 254 wp_safe_redirect(add_query_arg('wpchat_connect', $state, admin_url('admin.php?page=wp-chat'))); 255 exit(); 256 } 257 258 /** 259 * Save the parked access token after the admin confirms it. 260 * 261 * The admin_init hook also fires on admin-post.php and admin-ajax.php, including for logged-out 262 * requests, so the capability and nonce checks here are the only guard (SMASH-2676). 263 * 264 * @return void 265 */ 266 public function handleAccessToken() 267 { 268 // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Presence check only; the nonce is verified below before any write. 269 if (!isset($_POST['wpchat_connect_confirm'])) { 270 return; 271 } 272 273 if (!$this->currentUserIsWpChatAdmin()) { 274 return; 275 } 276 277 check_admin_referer(self::CONNECT_TOKEN_NONCE_ACTION, 'wpchat_connect_nonce'); 278 279 $accessToken = $this->pendingTokenForForm(); 280 delete_transient(self::PENDING_TOKEN_TRANSIENT . get_current_user_id()); 281 282 if ('' === $accessToken) { 283 wp_safe_redirect(add_query_arg('wpchat_connect', 'none', admin_url('admin.php?page=wp-chat'))); 284 exit(); 285 } 286 287 $this->privateSettingsService->updateSetting('api_token', $accessToken); 288 289 // Hooks run before the redirect (vector generation, usage stats). 290 do_action('wpchat_api_token_updated', $accessToken); 291 292 wp_safe_redirect(admin_url('admin.php?page=wp-chat')); 293 exit(); 294 } 295 296 /** 297 * The token and one-time id parked for the current user by captureAccessTokenFromUrl(). 298 * Both are '' when nothing is parked. 299 * 300 * @return array{token: string, id: string} 301 */ 302 private function getPending(): array 303 { 304 $pending = get_transient(self::PENDING_TOKEN_TRANSIENT . get_current_user_id()); 305 if (!is_array($pending) || !isset($pending['token'], $pending['id'])) { 306 return array('token' => '', 'id' => ''); 307 } 308 309 return array('token' => (string) $pending['token'], 'id' => (string) $pending['id']); 310 } 311 312 /** 313 * The parked token, but only when the confirmed form carries its one-time id; '' otherwise. 314 * 315 * A link loaded after the Connect screen rendered replaces the parked token and its id, so 316 * the older screen's POST saves nothing (SMASH-2676). Call after the nonce check. 317 * 318 * @return string 319 */ 320 private function pendingTokenForForm(): string 321 { 322 $pending = $this->getPending(); 323 // phpcs:disable WordPress.Security.NonceVerification.Missing -- handleAccessToken() verified the nonce. 324 $postedId = isset($_POST['wpchat_connect_id']) 325 ? sanitize_text_field(wp_unslash($_POST['wpchat_connect_id'])) 326 : ''; 327 // phpcs:enable WordPress.Security.NonceVerification.Missing 328 329 // Nothing parked gives '' for both, so the token returned is '' too. 330 return hash_equals($pending['id'], $postedId) ? $pending['token'] : ''; 331 } 332 333 /** 334 * The access token the WPChat API put in the admin page URL, or '' when there is none. 335 * 336 * @return string 337 */ 338 private function getAccessTokenFromUrl(): string 339 { 340 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Parked for confirmation only; saving needs the nonce in handleAccessToken(). 341 if (!isset($_GET['access_token']) || !is_string($_GET['access_token'])) { 342 return ''; 343 } 344 345 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- As above. 346 return sanitize_text_field(wp_unslash($_GET['access_token'])); 347 } 348 349 /** 350 * Same check as the admin REST routes (RestEndpoint::checkPermission()). 351 * 352 * @return bool 353 */ 354 private function currentUserIsWpChatAdmin(): bool 355 { 356 return current_user_can('manage_options') || current_user_can('wpchat_admin'); 357 } 358 359 /** 360 * Remove admin notices on WPChat pages 193 361 * 194 362 * @return void 195 363 */ 196 public function handleAccessToken() { 197 // Check if we're on a WPChat admin page and have an access_token parameter 364 public function removeAdminNotices() { 198 365 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Read-only check of admin page parameter, no data modification 199 $currentPage = $_GET['page'] ?? ''; 200 $isWpChatPage = (strpos( $currentPage, 'wp-chat' ) === 0); 201 202 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- OAuth callback from external service cannot include nonce 203 if ( $isWpChatPage && isset( $_GET['access_token'] ) ) { 204 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.NonceVerification.Recommended -- OAuth access token from external service, properly sanitized 205 $accessToken = sanitize_text_field( wp_unslash( $_GET['access_token'] ) ); 206 207 // Validate that the token is not empty 208 if ( ! empty( $accessToken ) ) { 209 // Store the access token in private settings 210 $this->privateSettingsService->updateSetting( 'api_token', $accessToken ); 211 212 // Trigger any actions that need to happen after token update 213 // This ensures hooks are processed before redirect 214 do_action( 'wpchat_api_token_updated', $accessToken ); 215 216 // Redirect to remove the token from the URL for security 217 $redirectUrl = remove_query_arg( 'access_token' ); 218 wp_safe_redirect( $redirectUrl ); 219 exit(); 220 } 221 } 222 } 223 224 /** 225 * Remove admin notices on WPChat pages 226 * 227 * @return void 228 */ 229 public function removeAdminNotices() { 230 // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Read-only check of admin page parameter, no data modification 231 $currentPage = $_GET['page'] ?? ''; 232 $isWpChatPage = (strpos( $currentPage, 'wp-chat' ) === 0); 233 234 if ( $isWpChatPage ) { 235 remove_all_actions( 'admin_notices' ); 236 remove_all_actions( 'all_admin_notices' ); 237 } 238 } 366 $currentPage = $_GET['page'] ?? ''; 367 $isWpChatPage = (strpos( $currentPage, 'wp-chat' ) === 0); 368 369 if ( $isWpChatPage ) { 370 remove_all_actions( 'admin_notices' ); 371 remove_all_actions( 'all_admin_notices' ); 372 } 373 } 239 374 240 375 /** … … 247 382 * @return array Modified capabilities. 248 383 */ 249 public function filterUserCapabilities( array $allcaps, array $caps, array $args, $user ): array {250 if ( isset( $allcaps['manage_options'] ) && $allcaps['manage_options'] ) {251 $allcaps['wpchat_admin'] = true;252 }253 254 return $allcaps;255 }384 public function filterUserCapabilities( array $allcaps, array $caps, array $args, $user ): array { 385 if ( isset( $allcaps['manage_options'] ) && $allcaps['manage_options'] ) { 386 $allcaps['wpchat_admin'] = true; 387 } 388 389 return $allcaps; 390 } 256 391 257 392 /** … … 261 396 * @return array Modified capabilities array. 262 397 */ 263 public function registerMembersCapabilities( array $caps ): array { 264 $caps[] = 'wpchat_admin'; 265 266 return $caps; 398 public function registerMembersCapabilities( array $caps ): array { 399 $caps[] = 'wpchat_admin'; 400 401 return $caps; 402 } 267 403 } 268 } -
smashballoon-wpchat-livechat-customer-support/trunk/src/Common/Services/Database/FaqSearchService.php
r3760121 r3798396 8 8 use SmashBalloon\WPChat\Common\Services\VectorEmbeddingService; 9 9 use SmashBalloon\WPChat\Common\Services\SettingsService; 10 use SmashBalloon\WPChat\Common\Services\SmartSearchRateLimiter; 10 11 use Exception; 11 12 use wpdb; … … 50 51 51 52 /** 53 * Per-visitor limit on searches that spend the site's WPChat API tokens. 54 * 55 * @var SmartSearchRateLimiter 56 */ 57 protected SmartSearchRateLimiter $rateLimiter; 58 59 /** 52 60 * Constructor. 53 61 * 54 * @param wpdb $db The WordPress database object. 55 * @param SettingsService $settingsService The settings service. 56 * @param VectorEmbeddingService|null $vectorService The vector embedding service (optional). 57 */ 58 public function __construct(wpdb $db, SettingsService $settingsService, ?VectorEmbeddingService $vectorService = null) 59 { 62 * @param wpdb $db The WordPress database object. 63 * @param SettingsService $settingsService The settings service. 64 * @param VectorEmbeddingService|null $vectorService The vector embedding service (optional). 65 * @param SmartSearchRateLimiter|null $rateLimiter The smart search limiter (optional). 66 */ 67 public function __construct( 68 wpdb $db, 69 SettingsService $settingsService, 70 ?VectorEmbeddingService $vectorService = null, 71 ?SmartSearchRateLimiter $rateLimiter = null 72 ) { 60 73 $this->db = $db; 61 74 $this->tableName = $db->prefix . 'wpchat_faqs'; 62 75 $this->settingsService = $settingsService; 63 76 $this->vectorService = $vectorService; 77 $this->rateLimiter = $rateLimiter ?? new SmartSearchRateLimiter(); 64 78 } 65 79 … … 95 109 $smartSearchEnabled = (bool) apply_filters('wpchat_smart_search_enabled', $smartSearchEnabled, $query); 96 110 97 // Try vector search first if available and enabled 98 if ($this->vectorService && $smartSearchEnabled) { 111 // Try vector search first if available and enabled. Over the per-visitor limit, the search 112 // falls through to the local keyword search so it does not spend the site's tokens. 113 if ($this->vectorService && $smartSearchEnabled && $this->rateLimiter->attempt($query)) { 99 114 try { 100 115 $vectorResults = $this->vectorService->searchByVector($query, $options['limit'] ?? 10); -
smashballoon-wpchat-livechat-customer-support/trunk/src/Common/Services/SettingsService.php
r3760121 r3798396 61 61 public function register(): void { 62 62 } 63 64 /** Response-only settings fields that must never be stored or served publicly. */ 65 public const ADMIN_ONLY_KEYS = array( 'hasAccessToken', 'tokenUsage', 'newsletterStatus' ); 63 66 64 67 /** … … 173 176 } 174 177 175 return $merged_settings; 178 // Admin-only live values are added by SettingsEndpoint::getSettings(). 179 // Drop any stale stored copy so no caller can leak it to non-admins. 180 return array_diff_key( $merged_settings, array_flip( self::ADMIN_ONLY_KEYS ) ); 176 181 } 177 182 … … 281 286 */ 282 287 public function updateSettings(array $settings): bool { 283 $currentSettings = $this->repository->getSettings(); 288 // Drop any stale stored copy of the admin-only keys so this save cleans it. 289 $currentSettings = array_diff_key( $this->repository->getSettings(), array_flip( self::ADMIN_ONLY_KEYS ) ); 284 290 285 291 // Pro forces telemetry + in-plugin notifications on at read time, so never -
smashballoon-wpchat-livechat-customer-support/trunk/src/Common/Services/SingletonContainer.php
r3423106 r3798396 68 68 } 69 69 self::$instance->set( 70 SmartSearchRateLimiter::class, 71 autowire(SmartSearchRateLimiter::class) 72 ->constructorParameter('privateSettings', \SmashBalloon\WPChat\Vendor\DI\get(PrivateSettingsService::class)) 73 ); 74 self::$instance->set( 70 75 SearchServiceInterface::class, 71 76 autowire(FaqSearchService::class) 72 77 ->constructorParameter('vectorService', \SmashBalloon\WPChat\Vendor\DI\get(VectorEmbeddingService::class)) 78 ->constructorParameter('rateLimiter', \SmashBalloon\WPChat\Vendor\DI\get(SmartSearchRateLimiter::class)) 73 79 ); 74 80 -
smashballoon-wpchat-livechat-customer-support/trunk/src/Common/Services/SmartSearchManagerService.php
r3376414 r3798396 131 131 } 132 132 133 $subscriptionTime = new \DateTime($subscriptionDate); 134 $now = new \DateTime(); 135 $hoursDiff = ($now->getTimestamp() - $subscriptionTime->getTimestamp()) / 3600; 133 // claimOffer() stores current_time('mysql'), which is in the site's timezone. 134 try { 135 $subscriptionTime = new \DateTime((string) $subscriptionDate, wp_timezone()); 136 } catch (\Exception $e) { 137 return false; 138 } 139 140 $hoursDiff = (time() - $subscriptionTime->getTimestamp()) / 3600; 136 141 137 142 return $hoursDiff < 24; -
smashballoon-wpchat-livechat-customer-support/trunk/src/Common/Services/VectorEmbeddingService.php
r3567449 r3798396 16 16 class VectorEmbeddingService implements ServiceProviderInterface 17 17 { 18 /** 19 * Longest visitor query, in characters, sent to the embeddings API (SMASH-2676). 20 */ 21 public const SEARCH_QUERY_MAX_CHARS = 200; 22 18 23 private ?SQLite3 $db = null; 19 24 private string $vectorFilePath; … … 483 488 { 484 489 try { 485 $queryVector = $this->generateEmbedding($query); 490 // The query comes from a public route and is billed by length, so only the start is sent. 491 $queryVector = $this->generateEmbedding(mb_substr($query, 0, self::SEARCH_QUERY_MAX_CHARS)); 486 492 487 493 // Get similar vectors with their scores -
smashballoon-wpchat-livechat-customer-support/trunk/vendor/composer/installed.php
r3760121 r3798396 3 3 namespace SmashBalloon\WPChat\Vendor; 4 4 5 return array('root' => array('name' => 'smashballoon/wpchat', 'pretty_version' => 'v1.4. 5', 'version' => '1.4.5.0', 'reference' => '8203a421eef5907ec2f5238f839c6ae61762c22b', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => \false), 'versions' => array('laravel/serializable-closure' => array('pretty_version' => 'v1.3.7', 'version' => '1.3.7.0', 'reference' => '4f48ade902b94323ca3be7646db16209ec76be3d', 'type' => 'library', 'install_path' => __DIR__ . '/../laravel/serializable-closure', 'aliases' => array(), 'dev_requirement' => \false), 'php-di/invoker' => array('pretty_version' => '2.3.7', 'version' => '2.3.7.0', 'reference' => '3c1ddfdef181431fbc4be83378f6d036d59e81e1', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/invoker', 'aliases' => array(), 'dev_requirement' => \false), 'php-di/php-di' => array('dev_requirement' => \false, 'replaced' => array(0 => '6.4.0')), 'php-di/phpdoc-reader' => array('pretty_version' => '2.2.1', 'version' => '2.2.1.0', 'reference' => '66daff34cbd2627740ffec9469ffbac9f8c8185c', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/phpdoc-reader', 'aliases' => array(), 'dev_requirement' => \false), 'psr/container' => array('pretty_version' => '1.1.2', 'version' => '1.1.2.0', 'reference' => '513e0666f7216c7459170d56df27dfcefe1689ea', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/container', 'aliases' => array(), 'dev_requirement' => \false), 'smashballoon/framework' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '3c53a9a74cc5bbdb4df2cfec13007f4793404e0f', 'type' => 'library', 'install_path' => __DIR__ . '/../smashballoon/framework', 'aliases' => array(0 => '1.0.0', 1 => '9999999-dev'), 'dev_requirement' => \false), 'smashballoon/wpchat' => array('pretty_version' => 'v1.4.5', 'version' => '1.4.5.0', 'reference' => '8203a421eef5907ec2f5238f839c6ae61762c22b', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => \false)));5 return array('root' => array('name' => 'smashballoon/wpchat', 'pretty_version' => 'v1.4.6', 'version' => '1.4.6.0', 'reference' => 'e3d3b5df8cd2df0c43fb388fae64000e6936afd4', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev' => \false), 'versions' => array('laravel/serializable-closure' => array('pretty_version' => 'v1.3.7', 'version' => '1.3.7.0', 'reference' => '4f48ade902b94323ca3be7646db16209ec76be3d', 'type' => 'library', 'install_path' => __DIR__ . '/../laravel/serializable-closure', 'aliases' => array(), 'dev_requirement' => \false), 'php-di/invoker' => array('pretty_version' => '2.3.7', 'version' => '2.3.7.0', 'reference' => '3c1ddfdef181431fbc4be83378f6d036d59e81e1', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/invoker', 'aliases' => array(), 'dev_requirement' => \false), 'php-di/php-di' => array('dev_requirement' => \false, 'replaced' => array(0 => '6.4.0')), 'php-di/phpdoc-reader' => array('pretty_version' => '2.2.1', 'version' => '2.2.1.0', 'reference' => '66daff34cbd2627740ffec9469ffbac9f8c8185c', 'type' => 'library', 'install_path' => __DIR__ . '/../php-di/phpdoc-reader', 'aliases' => array(), 'dev_requirement' => \false), 'psr/container' => array('pretty_version' => '1.1.2', 'version' => '1.1.2.0', 'reference' => '513e0666f7216c7459170d56df27dfcefe1689ea', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/container', 'aliases' => array(), 'dev_requirement' => \false), 'smashballoon/framework' => array('pretty_version' => 'dev-master', 'version' => 'dev-master', 'reference' => '3c53a9a74cc5bbdb4df2cfec13007f4793404e0f', 'type' => 'library', 'install_path' => __DIR__ . '/../smashballoon/framework', 'aliases' => array(0 => '1.0.0', 1 => '9999999-dev'), 'dev_requirement' => \false), 'smashballoon/wpchat' => array('pretty_version' => 'v1.4.6', 'version' => '1.4.6.0', 'reference' => 'e3d3b5df8cd2df0c43fb388fae64000e6936afd4', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), 'dev_requirement' => \false))); -
smashballoon-wpchat-livechat-customer-support/trunk/wp-chat.php
r3760121 r3798396 5 5 * Plugin URI: https://wpchat.com 6 6 * Description: Add a click-to-chat button to your WordPress site so visitors can message you on WhatsApp, Messenger, Telegram, or Instagram in one click. 7 * Version: 1.4. 57 * Version: 1.4.6 8 8 * Author: Smash Balloon 9 9 * Author URI: https://smashballoon.com … … 23 23 } 24 24 if (!defined('WPCHAT_VERSION')) { 25 define('WPCHAT_VERSION', '1.4. 5');25 define('WPCHAT_VERSION', '1.4.6'); 26 26 } 27 27 if (!defined('WPCHAT_PLUGIN_FILE')) {
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)