Changeset 3381510
- Timestamp:
- 10/20/2025 07:43:21 PM (5 months ago)
- Location:
- synoveo/trunk
- Files:
-
- 4 edited
-
assets/js/google-business-connector.js (modified) (3 diffs)
-
includes/services/class-synoveo-api-service.php (modified) (3 diffs)
-
readme.txt (modified) (3 diffs)
-
synoveo.php (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
synoveo/trunk/assets/js/google-business-connector.js
r3378651 r3381510 144 144 145 145 /** 146 * Get or generate stable Lite user identifier 147 * Priority: WordPress option > localStorage > generate new 148 */ 149 getOrCreateUserIdentifier() { 150 // First, check if WordPress has it (source of truth) 151 let identifier = window.synoveo_ajax?.user_identifier; 152 153 // If WordPress has synoveo_lite_ key, use it 154 if (identifier && identifier.startsWith('synoveo_lite_')) { 155 localStorage.setItem('synoveo_lite_identifier', identifier); 156 return identifier; 157 } 158 159 // Check localStorage 160 identifier = localStorage.getItem('synoveo_lite_identifier'); 161 if (identifier && identifier.startsWith('synoveo_lite_')) { 162 return identifier; 163 } 164 165 // Generate new stable identifier (deterministic based on domain) 166 const domain = window.location.hostname 167 .replace(/[^a-z0-9]/g, '') 168 .substring(0, 8); 169 170 // Use deterministic hash based on domain + fixed salt 146 * Compute canonical Lite identifier based on hostname. 147 * 148 * Mirrors PHP and Node implementations to avoid divergence. 149 * 150 * @param {string} hostname 151 * @returns {string} 152 */ 153 computeLiteIdentifier(hostname) { 154 const source = typeof hostname === 'string' && hostname.length > 0 ? hostname : window.location.hostname; 155 let normalized = (source || '').toLowerCase().replace(/[^a-z0-9]/g, ''); 156 157 if (!normalized) { 158 normalized = 'synoveo'; 159 } 160 161 const prefix = (normalized + '00000000').substring(0, 8); 171 162 const salt = 'synoveo_stable_2025'; 172 const hashInput = domain + salt; 173 174 // Simple deterministic hash (same input = same output) 163 const hashInput = prefix + salt; 164 175 165 let hash = 0; 176 166 for (let i = 0; i < hashInput.length; i++) { 177 const char = hashInput.charCodeAt(i); 178 hash = ((hash << 5) - hash) + char; 179 hash = hash & hash; // Convert to 32-bit integer 180 } 181 182 // Convert to alphanumeric string 183 const hashStr = Math.abs(hash).toString(36).substring(0, 15).padEnd(15, '0'); 184 185 identifier = `synoveo_lite_${domain}${hashStr}`; 186 localStorage.setItem('synoveo_lite_identifier', identifier); 187 188 SynoveoLogger.info(`Generated stable identifier: ${identifier}`, 'GoogleBusiness'); 189 return identifier; 167 hash = ((hash << 5) - hash) + hashInput.charCodeAt(i); 168 hash |= 0; // Force 32-bit signed integer 169 } 170 171 let hashStr = Math.abs(hash).toString(36).toLowerCase(); 172 hashStr = (hashStr + '000000000000000').substring(0, 15); 173 174 return `synoveo_lite_${prefix}${hashStr}`; 175 } 176 177 /** 178 * Get or generate stable Lite user identifier. 179 * Always returns the canonical hostname-derived identifier. 180 */ 181 getOrCreateUserIdentifier() { 182 const canonical = this.computeLiteIdentifier(window.location.hostname); 183 184 try { 185 const optionIdentifier = window.synoveo_ajax?.user_identifier; 186 if (optionIdentifier && !optionIdentifier.startsWith('synoveo_lite_')) { 187 return optionIdentifier; 188 } 189 190 if (optionIdentifier && optionIdentifier.startsWith('synoveo_lite_')) { 191 if (optionIdentifier !== canonical) { 192 localStorage.setItem('synoveo_lite_identifier', canonical); 193 return canonical; 194 } 195 196 localStorage.setItem('synoveo_lite_identifier', optionIdentifier); 197 return optionIdentifier; 198 } 199 } catch (error) { 200 SynoveoLogger.warn(`Failed to sync WordPress option identifier: ${error.message}`, 'GoogleBusiness'); 201 } 202 203 try { 204 const stored = localStorage.getItem('synoveo_lite_identifier'); 205 if (stored && stored.startsWith('synoveo_lite_')) { 206 if (stored !== canonical) { 207 localStorage.setItem('synoveo_lite_identifier', canonical); 208 SynoveoLogger.info(`Normalized Lite identifier to canonical value: ${canonical}`, 'GoogleBusiness'); 209 return canonical; 210 } 211 212 return stored; 213 } 214 } catch (error) { 215 SynoveoLogger.warn(`Unable to access localStorage for Lite identifier: ${error.message}`, 'GoogleBusiness'); 216 } 217 218 try { 219 localStorage.setItem('synoveo_lite_identifier', canonical); 220 } catch (error) { 221 SynoveoLogger.warn(`Failed to persist canonical Lite identifier: ${error.message}`, 'GoogleBusiness'); 222 } 223 224 SynoveoLogger.info(`Generated canonical identifier: ${canonical}`, 'GoogleBusiness'); 225 return canonical; 190 226 } 191 227 … … 1230 1266 const sendBtn = document.getElementById('wizard-send-code-btn'); 1231 1267 1232 // Get or generate useridentifier1268 // Get or generate canonical Lite identifier 1233 1269 let userIdentifier = window.synoveo_ajax?.user_identifier; 1234 if (!userIdentifier || userIdentifier === '') { 1235 userIdentifier = localStorage.getItem('synoveo_lite_identifier'); 1236 if (!userIdentifier) { 1237 const siteIdentifier = window.location.hostname.replace(/[^a-z0-9]/g, '').substring(0, 8); 1238 const randomSuffix = Math.random().toString(36).substring(2, 15); 1239 userIdentifier = `synoveo_lite_${siteIdentifier}${randomSuffix}`; 1240 localStorage.setItem('synoveo_lite_identifier', userIdentifier); 1241 SynoveoLogger.debug(`Generated new Lite identifier: ${userIdentifier}`, 'GoogleBusiness'); 1242 } else { 1243 SynoveoLogger.debug(`Using existing identifier: ${userIdentifier}`, 'GoogleBusiness'); 1244 } 1245 } else { 1246 SynoveoLogger.debug(`Using API key: ${userIdentifier}`, 'GoogleBusiness'); 1247 } 1270 if (!userIdentifier || userIdentifier === '' || userIdentifier.startsWith('synoveo_lite_')) { 1271 userIdentifier = this.getOrCreateUserIdentifier(); 1272 } 1273 SynoveoLogger.debug(`Using identifier for verification: ${userIdentifier}`, 'GoogleBusiness'); 1248 1274 1249 1275 if (!email) { … … 1577 1603 const sendBtn = document.getElementById('send-code-btn'); 1578 1604 1579 // Get or generate useridentifier1605 // Get or generate canonical identifier 1580 1606 let userIdentifier = window.synoveo_ajax?.user_identifier; 1581 1582 // If no API key exists (new Lite user), check localStorage or generate one 1583 if (!userIdentifier || userIdentifier === '') { 1584 // Check if we already generated one in this session 1585 userIdentifier = localStorage.getItem('synoveo_lite_identifier'); 1586 1587 if (!userIdentifier) { 1588 // Generate a new Lite user identifier based on site/domain 1589 const siteIdentifier = window.location.hostname.replace(/[^a-z0-9]/g, '').substring(0, 8); 1590 const randomSuffix = Math.random().toString(36).substring(2, 15); 1591 userIdentifier = `synoveo_lite_${siteIdentifier}${randomSuffix}`; 1592 1593 // Store it in localStorage so it persists 1594 localStorage.setItem('synoveo_lite_identifier', userIdentifier); 1595 SynoveoLogger.debug(`Generated Lite user identifier: ${userIdentifier}`, 'GoogleBusiness'); 1596 } else { 1597 SynoveoLogger.debug(`Reusing Lite user identifier from localStorage: ${userIdentifier}`, 'GoogleBusiness'); 1598 } 1599 } 1607 if (!userIdentifier || userIdentifier === '' || userIdentifier.startsWith('synoveo_lite_')) { 1608 userIdentifier = this.getOrCreateUserIdentifier(); 1609 } 1610 SynoveoLogger.debug(`Using identifier for legacy verification flow: ${userIdentifier}`, 'GoogleBusiness'); 1600 1611 1601 1612 if (!email) { -
synoveo/trunk/includes/services/class-synoveo-api-service.php
r3378651 r3381510 218 218 * @return string The Lite user identifier in format synoveo_lite_<hash> 219 219 */ 220 private function getLiteApiKey(): string { 221 // PRIORITY: Use registered user_identifier from WordPress option (source of truth) 222 $stored_identifier = get_option( 'synoveo_user_identifier', '' ); 223 if ( ! empty( $stored_identifier ) && str_starts_with( $stored_identifier, 'synoveo_lite_' ) ) { 220 /** 221 * Public accessor for Lite API key generation. 222 * Called by bootstrap to self-heal user identifier on plugin load. 223 */ 224 public function getLiteApiKey(): string { 225 $stored_identifier = get_option( 'synoveo_user_identifier', '' ); 226 $canonical_identifier = $this->compute_lite_identifier(); 227 228 if ( is_string( $stored_identifier ) && str_starts_with( $stored_identifier, 'synoveo_lite_' ) ) { 229 if ( $stored_identifier !== $canonical_identifier ) { 230 update_option( 'synoveo_user_identifier', $canonical_identifier, false ); 231 return $canonical_identifier; 232 } 233 224 234 return $stored_identifier; 225 235 } 226 227 // Fallback: Generate deterministic key based on domain (same logic as backend) 228 $domain = wp_parse_url( home_url(), PHP_URL_HOST ); 229 $domain = strtolower( trim( $domain ) ); 230 $domain = preg_replace( '/[^a-z0-9.\-]/i', '', $domain ); 231 // Align with server hash logic (sha256(domain + salt) → 16 hex chars) 232 $hash = hash( 'sha256', $domain . 'synoveo_lite_salt_2025' ); 233 return 'synoveo_lite_' . substr( $hash, 0, 16 ); 236 237 update_option( 'synoveo_user_identifier', $canonical_identifier, false ); 238 return $canonical_identifier; 234 239 } 235 240 … … 643 648 */ 644 649 private function get_or_generate_lite_key(): string { 645 // Generate deterministic key based on domain 646 $domain = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : home_url(); 647 $domain = preg_replace( '/^https?:\/\//', '', $domain ); // Clean domain 648 $domain = preg_replace( '/[^a-zA-Z0-9.-]/', '', $domain ); // Sanitize 649 650 // Create deterministic hash 651 $hash = hash( 'sha256', $domain . 'synoveo_lite_salt_2025' ); 652 $short_hash = substr( $hash, 0, 16 ); // 16 chars 653 654 return 'synoveo_lite_' . $short_hash; 650 return $this->compute_lite_identifier(); 655 651 } 656 652 … … 759 755 ); 760 756 } 757 758 /** 759 * Compute canonical Lite user identifier based on the current site hostname. 760 * 761 * Matches frontend and backend logic to prevent divergent identifiers. 762 * 763 * @return string Lite identifier in the format synoveo_lite_<prefix><suffix> 764 */ 765 private function compute_lite_identifier(): string { 766 $host = wp_parse_url( home_url(), PHP_URL_HOST ); 767 if ( empty( $host ) ) { 768 $host = home_url(); 769 } 770 771 $normalized = strtolower( $host ); 772 $normalized = preg_replace( '/[^a-z0-9]/', '', $normalized ?? '' ); 773 if ( empty( $normalized ) ) { 774 $normalized = 'synoveo'; 775 } 776 777 $prefix = substr( $normalized . '00000000', 0, 8 ); 778 $salt = 'synoveo_stable_2025'; 779 780 $hash_input = $prefix . $salt; 781 $hash = 0; 782 783 foreach ( str_split( $hash_input ) as $char ) { 784 $hash = ( ( $hash << 5 ) - $hash ) + ord( $char ); 785 $hash &= 0xFFFFFFFF; 786 787 if ( $hash & 0x80000000 ) { 788 $hash -= 0x100000000; 789 } 790 } 791 792 $hash_value = abs( $hash ); 793 $hash_str = strtolower( base_convert( (string) $hash_value, 10, 36 ) ); 794 $hash_str = substr( $hash_str . '000000000000000', 0, 15 ); 795 796 return 'synoveo_lite_' . $prefix . $hash_str; 797 } 761 798 } -
synoveo/trunk/readme.txt
r3378651 r3381510 5 5 Tested up to: 6.8 6 6 Requires PHP: 7.4 7 Stable tag: 1.2. 07 Stable tag: 1.2.1 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 252 252 253 253 == Changelog == 254 255 = 1.2.1 = 256 * Fixed Lite user desynchronization issue between PHP and JS key generators. 257 * Added automatic self-healing of Lite identifier on plugin load. 258 * Improved bootstrap stability and idempotent registration flow. 259 * Minor logging improvements and internal API normalization. 254 260 255 261 = 1.2.0 (2024-03-15) = … … 344 350 == Upgrade Notice == 345 351 352 = 1.2.1 = 353 Lite identifier alignment and self-healing. Recommended for all Lite installations to prevent registration errors. 354 346 355 = 1.2.0 = 347 356 Major stability improvements. This update eliminates quota duplication issues and rate limiting errors. Recommended for all users. -
synoveo/trunk/synoveo.php
r3378651 r3381510 4 4 * Plugin URI: https://www.synoveo.com 5 5 * Description: Grow revenue by improving your online business presence. Synoveo keeps your Google Business Profile accurate and up-to-date. 6 * Version: 1.2. 06 * Version: 1.2.1 7 7 * Author: Synoveo (CODE75) 8 8 * Author URI: https://www.synoveo.com … … 111 111 // Plugin constants 112 112 // Single source of truth for version number 113 $base_version = '1.2. 0'; // ← Change only this when bumping version113 $base_version = '1.2.1'; // ← Change only this when bumping version 114 114 define( 'SYNOVEO_VERSION', defined( 'WP_DEBUG' ) && WP_DEBUG ? $base_version . '-dev-' . time() : $base_version ); 115 115 define( 'SYNOVEO_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); … … 203 203 $this->admin_controller = $this->container->get( 'admin_controller' ); 204 204 $this->frontend_service = $this->container->get( 'frontend_service' ); 205 $api_service = $this->container->get( 'api_service' ); 206 207 // Automatic Lite identifier self-healing on plugin load 208 update_option( 'synoveo_user_identifier', $api_service->getLiteApiKey(), true ); 205 209 206 210 // Configure Admin Controller
Note: See TracChangeset
for help on using the changeset viewer.