Changeset 3433224
- Timestamp:
- 01/06/2026 05:11:10 AM (3 months ago)
- Location:
- synoveo/trunk
- Files:
-
- 14 added
- 6 edited
-
direct-api.php (modified) (11 diffs)
-
includes/mappings (added)
-
includes/mappings/all-in-one-seo.php (added)
-
includes/mappings/amelia.php (added)
-
includes/mappings/bookly.php (added)
-
includes/mappings/business-profile.php (added)
-
includes/mappings/index.php (added)
-
includes/mappings/seo-by-rank-math.php (added)
-
includes/mappings/synoveo.php (added)
-
includes/mappings/woocommerce.php (added)
-
includes/mappings/wordpress-core.php (added)
-
includes/mappings/wordpress-seo.php (added)
-
includes/rest/handlers/class-synoveo-business-data-handler.php (modified) (1 diff)
-
includes/rest/handlers/class-synoveo-capabilities-handler.php (modified) (7 diffs)
-
includes/services/class-synoveo-option-batch-reader.php (added)
-
includes/services/class-synoveo-option-batch-writer.php (added)
-
includes/services/class-synoveo-option-mapping.php (modified) (2 diffs)
-
includes/services/class-synoveo-value-transforms.php (added)
-
readme.txt (modified) (1 diff)
-
synoveo.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
synoveo/trunk/direct-api.php
r3424860 r3433224 1 1 <?php 2 2 /** 3 * Direct API endpoint for Synoveo - Ultra-fast data extraction .3 * Direct API endpoint for Synoveo - Ultra-fast data extraction and import. 4 4 * 5 5 * This file provides high-performance REST API responses by using WordPress 6 6 * SHORTINIT mode, loading only the database layer without plugins or themes. 7 * This results in significantly faster response times for data extraction.7 * This results in significantly faster response times for data operations. 8 8 * 9 9 * @package Synoveo … … 11 11 * 12 12 * Usage (from plugin directory): 13 * /wp-content/plugins/synoveo/direct-api.php?route=/synoveo/v1/snapshot 14 * /wp-content/plugins/synoveo/direct-api.php?route=/synoveo/v1/sources/{plugin}/{field} 13 * GET /wp-content/plugins/synoveo/direct-api.php?route=/synoveo/v1/snapshot 14 * GET /wp-content/plugins/synoveo/direct-api.php?route=/synoveo/v1/sources/{plugin}/{field} 15 * POST /wp-content/plugins/synoveo/direct-api.php?route=/synoveo/v1/import 16 * Body: { "sources": { "plugin-key": { "gbp.field": value } } } 17 * GET /wp-content/plugins/synoveo/direct-api.php?route=/synoveo/v1/debug 15 18 */ 16 19 … … 46 49 // Pattern 2: /synoveo/v1/sources (list all). 47 50 $route_valid = true; 51 } elseif ( '/synoveo/v1/import' === $route ) { 52 // Pattern 3: /synoveo/v1/import (POST: write GBP data to WordPress). 53 $route_valid = true; 54 } elseif ( '/synoveo/v1/debug' === $route ) { 55 // Pattern 4: /synoveo/v1/debug (diagnostic info). 56 $route_valid = true; 48 57 } elseif ( preg_match( '#^/synoveo/v1/sources/([a-zA-Z0-9_-]+)/([a-zA-Z0-9_./]+)$#', $route, $matches ) ) { 49 // Pattern 3: /synoveo/v1/sources/{plugin}/{field}.58 // Pattern 5: /synoveo/v1/sources/{plugin}/{field}. 50 59 // Field can contain dots, slashes and uppercase (e.g., profile.description, attributes/url_instagram). 51 60 $route_valid = true; … … 160 169 require_once __DIR__ . '/includes/services/class-synoveo-plugin-registry.php'; 161 170 171 // Load modular plugin mappings infrastructure. 172 require_once __DIR__ . '/includes/services/class-synoveo-option-batch-reader.php'; 173 require_once __DIR__ . '/includes/services/class-synoveo-option-batch-writer.php'; 174 require_once __DIR__ . '/includes/services/class-synoveo-value-transforms.php'; 175 require_once __DIR__ . '/includes/mappings/index.php'; 176 162 177 /** 163 178 * Check if a plugin is active by reading the active_plugins option. … … 376 391 377 392 /** 393 * Extract business hours from Amelia Booking weekSchedule. 394 * Converts Amelia's format to GBP regularHours format. 395 * 396 * Amelia format: [{ "day": "Monday", "time": ["07:00", "20:00"] }, ...] 397 * GBP format: { periods: [{ openDay: "MONDAY", openTime: {hours: 7, minutes: 0}, ... }] } 398 * 399 * @param array $amelia_settings Parsed amelia_settings JSON option. 400 * @return array|null GBP regularHours format or null if no hours found. 401 */ 402 function synoveo_direct_extract_amelia_hours( $amelia_settings ) { 403 if ( empty( $amelia_settings ) || ! is_array( $amelia_settings ) ) { 404 return null; 405 } 406 407 $week_schedule = $amelia_settings['weekSchedule'] ?? null; 408 if ( empty( $week_schedule ) || ! is_array( $week_schedule ) ) { 409 return null; 410 } 411 412 $day_map = array( 413 'Monday' => 'MONDAY', 414 'Tuesday' => 'TUESDAY', 415 'Wednesday' => 'WEDNESDAY', 416 'Thursday' => 'THURSDAY', 417 'Friday' => 'FRIDAY', 418 'Saturday' => 'SATURDAY', 419 'Sunday' => 'SUNDAY', 420 ); 421 422 $periods = array(); 423 foreach ( $week_schedule as $entry ) { 424 $day = $entry['day'] ?? ''; 425 $time = $entry['time'] ?? array(); 426 427 if ( empty( $day ) || empty( $time ) || ! is_array( $time ) || count( $time ) < 2 ) { 428 continue; 429 } 430 431 $gbp_day = $day_map[ $day ] ?? null; 432 if ( ! $gbp_day ) { 433 continue; 434 } 435 436 $open_time = $time[0] ?? ''; 437 $close_time = $time[1] ?? ''; 438 439 if ( empty( $open_time ) || empty( $close_time ) ) { 440 continue; 441 } 442 443 $periods[] = array( 444 'openDay' => $gbp_day, 445 'openTime' => synoveo_time_to_gbp_object( $open_time ), 446 'closeDay' => $gbp_day, 447 'closeTime' => synoveo_time_to_gbp_object( $close_time ), 448 ); 449 } 450 451 return ! empty( $periods ) ? array( 'periods' => $periods ) : null; 452 } 453 454 /** 455 * Extract business hours from Bookly individual options. 456 * Converts Bookly's format to GBP regularHours format. 457 * 458 * Bookly stores hours in individual options: 459 * - bookly_bh_monday_start, bookly_bh_monday_end, etc. 460 * 461 * @param array $prefetched Prefetched options from batch reader. 462 * @return array|null GBP regularHours format or null if no hours found. 463 */ 464 function synoveo_direct_extract_bookly_hours( $prefetched ) { 465 $days = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' ); 466 $day_map = array( 467 'monday' => 'MONDAY', 468 'tuesday' => 'TUESDAY', 469 'wednesday' => 'WEDNESDAY', 470 'thursday' => 'THURSDAY', 471 'friday' => 'FRIDAY', 472 'saturday' => 'SATURDAY', 473 'sunday' => 'SUNDAY', 474 ); 475 476 $periods = array(); 477 foreach ( $days as $day ) { 478 $start_key = "bookly_bh_{$day}_start"; 479 $end_key = "bookly_bh_{$day}_end"; 480 481 $open_time = $prefetched[ $start_key ] ?? ''; 482 $close_time = $prefetched[ $end_key ] ?? ''; 483 484 // Skip if no hours set for this day (closed). 485 if ( empty( $open_time ) || empty( $close_time ) ) { 486 continue; 487 } 488 489 $periods[] = array( 490 'openDay' => $day_map[ $day ], 491 'openTime' => synoveo_time_to_gbp_object( $open_time ), 492 'closeDay' => $day_map[ $day ], 493 'closeTime' => synoveo_time_to_gbp_object( $close_time ), 494 ); 495 } 496 497 return ! empty( $periods ) ? array( 'periods' => $periods ) : null; 498 } 499 500 /** 378 501 * Get list of active plugins that we support. 379 502 * Uses Plugin_Registry as the SINGLE SOURCE OF TRUTH for plugin file mappings. … … 407 530 408 531 /** 409 * Extract all business data from wp_options (no plugin loading needed!) 410 * IMPORTANT: Only extracts data from ACTIVE plugins to avoid orphaned data confusion 532 * Get list of active plugins from modular registry. 533 * Dynamically checks plugin activation - no hardcoding needed. 534 * 535 * @return string[] Active plugin keys. 536 */ 537 function synoveo_get_active_plugin_keys() { 538 // Always-on plugins (no plugin_file check needed). 539 $active = array( 'wordpress-core', 'synoveo' ); 540 541 // Check each registered plugin for activation. 542 $all_mappings = SYNOVEO_Plugin_Mappings::load_all(); 543 foreach ( $all_mappings as $key => $mapping ) { 544 if ( in_array( $key, $active, true ) ) { 545 continue; 546 } 547 548 $plugin_file = isset( $mapping['plugin_file'] ) ? $mapping['plugin_file'] : null; 549 // Pass the plugin KEY (slug), not the file path. 550 // synoveo_direct_is_plugin_active() expects a slug and looks up the file path internally. 551 if ( $plugin_file && synoveo_direct_is_plugin_active( $key ) ) { 552 $active[] = $key; 553 } 554 } 555 556 return $active; 557 } 558 559 /** 560 * Export plugin snapshot data using modular mappings. 561 * Uses batch reader for optimized DB access (1 query vs N queries). 562 * Applies transform layer for value normalization. 563 * Re-filters after transforms to remove null/empty values. 564 * 565 * IMPORTANT: Also handles special fields that aren't stored in wp_options: 566 * - regularHours: Yoast stores as individual opening_hours_{day}_{from/to} options 567 * - serviceItems: Stored in database tables (Amelia, Bookly, WooCommerce) 568 * 569 * @param wpdb $wpdb WordPress database object. 570 * @param string[] $active_plugins Active plugin keys. 571 * @return array<string, array> Snapshot sources keyed by plugin. 572 */ 573 function synoveo_export_snapshot_sources( $wpdb, array $active_plugins ) { 574 // Step 1: Collect all required option names. 575 $option_names = SYNOVEO_Plugin_Mappings::list_required_options( $active_plugins ); 576 577 // Step 2: Batch fetch ALL options in ONE query. 578 $prefetched = SYNOVEO_Option_Batch_Reader::fetch_many( $wpdb, $option_names ); 579 580 // Step 3: Build snapshot for each plugin using prefetched data. 581 $sources = array(); 582 foreach ( $active_plugins as $plugin_key ) { 583 $fields = SYNOVEO_Plugin_Mappings::get_fields( $plugin_key ); 584 $data = array(); 585 586 foreach ( $fields as $gbp_field => $wp_path ) { 587 $value = SYNOVEO_Plugin_Mappings::get_value_from_prefetched( $plugin_key, $gbp_field, $prefetched ); 588 // For "synoveo" native plugin, include ALL fields (even empty) so dashboard knows they're available. 589 // For other plugins, only include fields with actual values. 590 if ( 'synoveo' === $plugin_key ) { 591 // Include field even if empty - dashboard needs to know synoveo can provide this field. 592 $data[ $gbp_field ] = ( null !== $value && '' !== $value ) ? $value : null; 593 } elseif ( null !== $value && '' !== $value ) { 594 // Flat key: phoneNumbers.primaryPhone (not nested object). 595 $data[ $gbp_field ] = $value; 596 } 597 } 598 599 // Step 3b: Handle special fields not in wp_options. 600 // regularHours for Yoast SEO (stored as individual opening_hours_* options). 601 if ( 'wordpress-seo' === $plugin_key ) { 602 $yoast_local = isset( $prefetched['wpseo_local'] ) ? $prefetched['wpseo_local'] : null; 603 $hours = synoveo_direct_extract_yoast_hours( $yoast_local ); 604 if ( $hours ) { 605 $data['regularHours'] = $hours; 606 } 607 } 608 609 // regularHours for Amelia Booking (stored in amelia_settings:weekSchedule). 610 if ( 'ameliabooking' === $plugin_key ) { 611 $amelia_settings = isset( $prefetched['amelia_settings'] ) ? $prefetched['amelia_settings'] : null; 612 $hours = synoveo_direct_extract_amelia_hours( $amelia_settings ); 613 if ( $hours ) { 614 $data['regularHours'] = $hours; 615 } 616 } 617 618 // regularHours for Bookly (stored in individual bookly_bh_{day}_start/end options). 619 if ( 'bookly-responsive-appointment-booking-tool' === $plugin_key ) { 620 $hours = synoveo_direct_extract_bookly_hours( $prefetched ); 621 if ( $hours ) { 622 $data['regularHours'] = $hours; 623 } 624 } 625 626 // serviceItems for booking plugins (stored in database tables). 627 if ( 'ameliabooking' === $plugin_key ) { 628 $services = synoveo_direct_get_amelia_services(); 629 if ( $services ) { 630 $data['serviceItems'] = $services; 631 } 632 } elseif ( 'bookly-responsive-appointment-booking-tool' === $plugin_key ) { 633 $services = synoveo_direct_get_bookly_services(); 634 if ( $services ) { 635 $data['serviceItems'] = $services; 636 } 637 } elseif ( 'woocommerce' === $plugin_key ) { 638 $services = synoveo_direct_get_woocommerce_services(); 639 if ( $services ) { 640 $data['serviceItems'] = $services; 641 } 642 } 643 644 if ( ! empty( $data ) ) { 645 // Step 4: Apply plugin-specific transforms (e.g., WooCommerce CC:STATE split). 646 // Pass prefetched in case transform needs access to raw options. 647 $data = SYNOVEO_Value_Transforms::apply( $plugin_key, $data, $prefetched ); 648 649 // Step 5: Re-filter after transforms. 650 // Transforms can create new keys or set values to null. 651 // Exception: For synoveo plugin, keep null values so dashboard knows fields are available. 652 foreach ( $data as $k => $v ) { 653 if ( 'synoveo' === $plugin_key ) { 654 // Keep synoveo fields even if null - dashboard needs to know they're available. 655 continue; 656 } 657 if ( null === $v ) { 658 unset( $data[ $k ] ); 659 } elseif ( is_string( $v ) && '' === trim( $v ) ) { 660 unset( $data[ $k ] ); 661 } elseif ( is_array( $v ) && empty( $v ) ) { 662 unset( $data[ $k ] ); 663 } 664 } 665 666 if ( ! empty( $data ) ) { 667 $sources[ $plugin_key ] = $data; 668 } 669 } 670 } 671 672 return $sources; 673 } 674 675 /** 676 * Import GBP data to WordPress options using modular mappings. 677 * Uses batch writer for optimized DB access. 678 * Applies reverse transforms for plugin-specific format conversion. 679 * 680 * @param wpdb $wpdb WordPress database object. 681 * @param string $plugin_key Target plugin to write to. 682 * @param array $gbp_data GBP field => value pairs. 683 * @return array Result with success status and details. 684 */ 685 function synoveo_import_to_plugin( $wpdb, $plugin_key, array $gbp_data ) { 686 $results = array(); 687 688 // Special handling for serviceItems. 689 if ( isset( $gbp_data['serviceItems'] ) && is_array( $gbp_data['serviceItems'] ) ) { 690 if ( 'synoveo' === $plugin_key ) { 691 // Synoveo stores serviceItems as JSON in wp_options. 692 // Use json_encode directly for SHORTINIT compatibility. 693 $json_value = json_encode( $gbp_data['serviceItems'] ); 694 $option_name = 'synoveo_service_items'; 695 696 // Check if option exists. 697 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 698 $exists = $wpdb->get_var( 699 $wpdb->prepare( 700 "SELECT option_id FROM {$wpdb->options} WHERE option_name = %s LIMIT 1", 701 $option_name 702 ) 703 ); 704 705 if ( $exists ) { 706 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 707 $result = $wpdb->update( 708 $wpdb->options, 709 array( 'option_value' => $json_value ), 710 array( 'option_name' => $option_name ), 711 array( '%s' ), 712 array( '%s' ) 713 ); 714 } else { 715 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 716 $result = $wpdb->insert( 717 $wpdb->options, 718 array( 719 'option_name' => $option_name, 720 'option_value' => $json_value, 721 'autoload' => 'no', 722 ), 723 array( '%s', '%s', '%s' ) 724 ); 725 } 726 $results['serviceItems'] = false !== $result; 727 } else { 728 // Booking plugins store serviceItems in database tables. 729 $service_result = synoveo_import_service_items( $wpdb, $plugin_key, $gbp_data['serviceItems'] ); 730 $results['serviceItems'] = $service_result['success']; 731 } 732 unset( $gbp_data['serviceItems'] ); 733 } 734 735 // Only proceed with options if there's remaining data. 736 if ( ! empty( $gbp_data ) ) { 737 // Step 1: Apply reverse transforms (GBP → WordPress format). 738 // e.g., for WooCommerce: regionCode + administrativeArea → CC:STATE. 739 $transformed = SYNOVEO_Value_Transforms::reverse_apply( $plugin_key, $gbp_data ); 740 741 // Step 2: Write to WordPress options using batch writer. 742 $option_results = SYNOVEO_Option_Batch_Writer::write_many( $wpdb, $plugin_key, $transformed ); 743 $results = array_merge( $results, $option_results ); 744 } 745 746 // Step 3: Build response. 747 $success_count = count( array_filter( $results ) ); 748 $total_count = count( $results ); 749 750 return array( 751 'success' => $success_count === $total_count, 752 'plugin' => $plugin_key, 753 'fields' => $total_count, 754 'written' => $success_count, 755 'failed' => $total_count - $success_count, 756 'details' => $results, 757 ); 758 } 759 760 /** 761 * Import serviceItems to booking plugin database tables. 762 * Matches services by name and updates price only (doesn't create new services). 763 * Uses modular mappings for table configuration (single source of truth). 764 * 765 * @param wpdb $wpdb WordPress database object. 766 * @param string $plugin_key Plugin identifier (ameliabooking, bookly-responsive-appointment-booking-tool). 767 * @param array $service_items GBP serviceItems array. 768 * @return array Result with success status. 769 */ 770 function synoveo_import_service_items( $wpdb, $plugin_key, array $service_items ) { 771 $updated = 0; 772 $failed = 0; 773 774 // Get table configuration from modular mappings (single source of truth). 775 $mapping_config = SYNOVEO_Plugin_Mappings::get_table_config( $plugin_key, 'serviceItems' ); 776 777 if ( ! $mapping_config ) { 778 return array( 779 'success' => false, 780 'error' => 'No serviceItems table mapping found for plugin: ' . $plugin_key, 781 'updated' => 0, 782 'failed' => count( $service_items ), 783 ); 784 } 785 786 // Build table config with prefixed table name. 787 $table_config = array( 788 'table' => $wpdb->prefix . $mapping_config['table'], 789 'name_col' => $mapping_config['name_col'], 790 'price_col' => $mapping_config['price_col'], 791 'status_col' => $mapping_config['status_col'], 792 'status_val' => $mapping_config['status_val'], 793 ); 794 795 // Check if table exists. 796 $table_exists = $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table_config['table'] ) ); 797 if ( $table_exists !== $table_config['table'] ) { 798 return array( 799 'success' => false, 800 'error' => 'Table not found: ' . $table_config['table'], 801 'updated' => 0, 802 'failed' => count( $service_items ), 803 ); 804 } 805 806 // Process each service item. 807 foreach ( $service_items as $item ) { 808 // Extract service name and price from GBP format. 809 $name = null; 810 $price = null; 811 812 if ( isset( $item['freeFormServiceItem']['label']['displayName'] ) ) { 813 $name = $item['freeFormServiceItem']['label']['displayName']; 814 } 815 if ( isset( $item['price'] ) ) { 816 $units = isset( $item['price']['units'] ) ? (int) $item['price']['units'] : 0; 817 $nanos = isset( $item['price']['nanos'] ) ? (int) $item['price']['nanos'] : 0; 818 $price = $units + ( $nanos / 1000000000 ); 819 } 820 821 if ( ! $name || null === $price ) { 822 ++$failed; 823 continue; 824 } 825 826 // Find existing service by name (case-insensitive match). 827 // Note: Table/column names come from our own config (not user input), so safe to interpolate. 828 // Using backticks directly because %i placeholder may not work in SHORTINIT mode. 829 $table = $table_config['table']; 830 $name_col = $table_config['name_col']; 831 $status_col = $table_config['status_col']; 832 $status_val = $table_config['status_val']; 833 834 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared 835 $existing = $wpdb->get_row( 836 $wpdb->prepare( 837 "SELECT id FROM `{$table}` WHERE LOWER(`{$name_col}`) = LOWER(%s) AND `{$status_col}` = %s LIMIT 1", 838 $name, 839 $status_val 840 ), 841 ARRAY_A 842 ); 843 844 if ( ! $existing ) { 845 // Service not found - create new service. 846 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 847 $result = $wpdb->insert( 848 $table_config['table'], 849 array( 850 $table_config['name_col'] => $name, 851 $table_config['price_col'] => $price, 852 $table_config['status_col'] => $status_val, 853 ), 854 array( '%s', '%f', '%s' ) 855 ); 856 857 if ( false !== $result ) { 858 ++$updated; 859 } else { 860 ++$failed; 861 } 862 } else { 863 // Service exists - update price. 864 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching 865 $result = $wpdb->update( 866 $table_config['table'], 867 array( $table_config['price_col'] => $price ), 868 array( 'id' => $existing['id'] ), 869 array( '%f' ), 870 array( '%d' ) 871 ); 872 873 if ( false !== $result ) { 874 ++$updated; 875 } else { 876 ++$failed; 877 } 878 } 879 } 880 881 return array( 882 'success' => $failed === 0, 883 'updated' => $updated, 884 'failed' => $failed, 885 ); 886 } 887 888 /** 889 * Handle import request - write GBP data to multiple WordPress plugins. 890 * 891 * @return array Import results. 892 */ 893 function synoveo_handle_import() { 894 global $wpdb; 895 896 // Get JSON body. 897 $input = file_get_contents( 'php://input' ); 898 if ( empty( $input ) ) { 899 return array( 900 'success' => false, 901 'error' => 'Empty request body', 902 ); 903 } 904 905 $data = json_decode( $input, true ); 906 if ( JSON_ERROR_NONE !== json_last_error() ) { 907 return array( 908 'success' => false, 909 'error' => 'Invalid JSON: ' . json_last_error_msg(), 910 ); 911 } 912 913 // Validate structure: { "sources": { "plugin-key": { "gbp.field": value } } } 914 if ( ! isset( $data['sources'] ) || ! is_array( $data['sources'] ) ) { 915 return array( 916 'success' => false, 917 'error' => 'Missing or invalid "sources" key. Expected: { "sources": { "plugin-key": { "field": value } } }', 918 ); 919 } 920 921 $results = array(); 922 $total_success = 0; 923 $total_failed = 0; 924 925 foreach ( $data['sources'] as $plugin_key => $gbp_data ) { 926 if ( ! is_array( $gbp_data ) ) { 927 $results[ $plugin_key ] = array( 928 'success' => false, 929 'error' => 'Invalid data format for plugin', 930 ); 931 ++$total_failed; 932 continue; 933 } 934 935 // Check if plugin has mapping. 936 $fields = SYNOVEO_Plugin_Mappings::get_fields( $plugin_key ); 937 if ( empty( $fields ) ) { 938 $results[ $plugin_key ] = array( 939 'success' => false, 940 'error' => 'No mapping found for plugin: ' . $plugin_key, 941 ); 942 ++$total_failed; 943 continue; 944 } 945 946 // Import to plugin. 947 $result = synoveo_import_to_plugin( $wpdb, $plugin_key, $gbp_data ); 948 $results[ $plugin_key ] = $result; 949 950 if ( $result['success'] ) { 951 ++$total_success; 952 } else { 953 ++$total_failed; 954 } 955 } 956 957 return array( 958 'success' => 0 === $total_failed, 959 'plugins_total' => count( $data['sources'] ), 960 'plugins_ok' => $total_success, 961 'plugins_failed' => $total_failed, 962 'results' => $results, 963 ); 964 } 965 966 /** 967 * Generate debug information for troubleshooting. 968 * 969 * @return array Debug information. 970 */ 971 function synoveo_get_debug_info() { 972 global $wpdb; 973 974 // Get all registered mappings. 975 $all_mappings = SYNOVEO_Plugin_Mappings::load_all(); 976 $active_plugins = synoveo_get_active_plugin_keys(); 977 978 // Build plugin info. 979 $plugins_info = array(); 980 foreach ( $all_mappings as $key => $mapping ) { 981 $is_active = in_array( $key, $active_plugins, true ); 982 $fields = isset( $mapping['fields'] ) ? $mapping['fields'] : array(); 983 984 $plugins_info[ $key ] = array( 985 'display_name' => $mapping['display_name'] ?? $key, 986 'plugin_file' => $mapping['plugin_file'] ?? null, 987 'active' => $is_active, 988 'fields_count' => count( $fields ), 989 'mapped_fields' => array_keys( $fields ), 990 ); 991 } 992 993 // Get WordPress install info. 994 $wp_info = array( 995 'table_prefix' => $wpdb->prefix, 996 'options_table' => $wpdb->options, 997 ); 998 999 // List all option names used by mappings. 1000 $required_options = SYNOVEO_Plugin_Mappings::list_required_options( array_keys( $all_mappings ) ); 1001 1002 // Check which options exist. 1003 $existing_options = array(); 1004 if ( ! empty( $required_options ) ) { 1005 $prefetched = SYNOVEO_Option_Batch_Reader::fetch_many( $wpdb, $required_options ); 1006 $existing_options = array_keys( $prefetched ); 1007 } 1008 1009 return array( 1010 'version' => '2.0', 1011 'php_version' => PHP_VERSION, 1012 'wordpress' => $wp_info, 1013 'mappings_loaded' => count( $all_mappings ), 1014 'active_plugins' => $active_plugins, 1015 'plugins' => $plugins_info, 1016 'required_options' => $required_options, 1017 'existing_options' => $existing_options, 1018 'missing_options' => array_diff( $required_options, $existing_options ), 1019 ); 1020 } 1021 1022 /** 1023 * Extract all business data from wp_options using modular mappings. 1024 * Uses batch reader for optimized DB access (1 query vs N queries). 1025 * 1026 * IMPORTANT: Only extracts data from ACTIVE plugins to avoid orphaned data confusion. 1027 * 1028 * Version 2.0: Uses modular plugin mappings with flat GBP field keys. 1029 * - Add new plugin = add mapping file (no code changes) 1030 * - Transform layer handles plugin-specific format normalization 1031 * - Batch reader minimizes DB queries 411 1032 */ 412 1033 function synoveo_direct_extract_snapshot() { 413 1034 global $wpdb; 414 1035 415 // Get only active plugins - this prevents showing orphaned data from deactivated plugins.416 $active_plugins = synoveo_get_active_supported_plugins();417 418 // Debug: check if API credentials are stored.419 $stored_client_id = synoveo_direct_get_option( 'synoveo_client_id', '' );420 $has_credentials = ! empty( $stored_client_id );421 422 1036 $snapshot = array( 423 'sources' => array(), 424 'extracted_at' => gmdate( 'c' ), 425 'active_plugins' => $active_plugins, 426 'has_credentials' => $has_credentials, 427 'client_id_prefix' => $has_credentials ? substr( $stored_client_id, 0, 20 ) . '...' : null, 428 ); 429 430 // WordPress Core - always included. 431 $snapshot['sources']['wordpress-core'] = array( 432 'title' => synoveo_get_mapped_value( 'wordpress-core', 'title' ), 433 'profile' => array( 434 'description' => synoveo_get_mapped_value( 'wordpress-core', 'profile.description' ), 435 ), 436 'websiteUri' => synoveo_get_mapped_value( 'wordpress-core', 'websiteUri' ), 437 'admin_email' => synoveo_get_mapped_value( 'wordpress-core', 'email' ), 438 ); 439 440 // WooCommerce - only if active. 441 if ( synoveo_direct_is_plugin_active( 'woocommerce' ) ) { 442 $wc_address = synoveo_get_mapped_value( 'woocommerce', 'addressLines' ); 443 if ( $wc_address ) { 444 // WooCommerce stores country:state together (e.g., "US:CA"), needs parsing. 445 $country_state = synoveo_get_mapped_value( 'woocommerce', 'regionCode' ) ?? ''; 446 $country_code = ''; 447 $state = ''; 448 if ( strpos( $country_state, ':' ) !== false ) { 449 list($country_code, $state) = explode( ':', $country_state ); 450 } else { 451 $country_code = $country_state; 452 } 453 454 // Build addressLines array. 455 $address_line_2 = synoveo_direct_get_option( 'woocommerce_store_address_2', '' ); 456 457 $wc_source = array( 458 'title' => synoveo_get_mapped_value( 'woocommerce', 'title' ), 459 'storefrontAddress' => array( 460 'addressLines' => array_filter( array( $wc_address, $address_line_2 ) ), 461 'locality' => synoveo_get_mapped_value( 'woocommerce', 'locality' ), 462 'administrativeArea' => $state, 463 'postalCode' => synoveo_get_mapped_value( 'woocommerce', 'postalCode' ), 464 'regionCode' => $country_code, 465 ), 466 'phoneNumbers' => array( 467 'primaryPhone' => synoveo_get_mapped_value( 'woocommerce', 'primaryPhone' ), 468 ), 469 'profile' => array( 470 'description' => synoveo_get_mapped_value( 'woocommerce', 'profile.description' ), 471 ), 472 'websiteUri' => synoveo_get_mapped_value( 'woocommerce', 'websiteUri' ), 473 ); 474 475 // Add products/services from database - GBP API field: serviceItems. 476 $wc_services = synoveo_direct_get_woocommerce_services(); 477 if ( $wc_services ) { 478 $wc_source['serviceItems'] = $wc_services; 479 } 480 481 // Add shop action links. 482 $shop_page_id = synoveo_direct_get_option( 'woocommerce_shop_page_id', 0 ); 483 $home_url = rtrim( synoveo_direct_get_option( 'home', '' ), '/' ); 484 485 if ( $shop_page_id ) { 486 $shop_slug = $wpdb->get_var( 487 $wpdb->prepare( 488 "SELECT post_name FROM {$wpdb->posts} WHERE ID = %d AND post_status = 'publish'", 489 $shop_page_id 490 ) 491 ); 492 $shop_url = $shop_slug ? "{$home_url}/{$shop_slug}/" : "{$home_url}/shop/"; 493 } else { 494 $shop_url = "{$home_url}/shop/"; 495 } 496 497 $wc_source['actionLinks'] = array( 498 'shop' => $shop_url, 499 'foodOrdering' => $shop_url, 500 'foodDelivery' => $shop_url, 501 'foodTakeout' => $shop_url, 502 ); 503 504 // NOTE: WooCommerce does not have GBP-compliant business categories. 505 // WooCommerce product categories (product_cat taxonomy) are NOT the same as. 506 // GBP business categories (predefined by Google: "Café", "Restaurant", etc.). 507 // Categories can only be synced GBP → WordPress, not WordPress → GBP. 508 // See: docs/wordpress/singlePluginSource.md. 509 510 $snapshot['sources']['woocommerce'] = $wc_source; 511 } 512 } 513 514 // Yoast SEO - only if active. 515 if ( synoveo_direct_is_plugin_active( 'wordpress-seo' ) ) { 516 $yoast_social = synoveo_direct_get_option( 'wpseo_social', array() ); 517 $yoast_local = synoveo_direct_get_option( 'wpseo_local', array() ); 518 if ( ! empty( $yoast_social ) || ! empty( $yoast_local ) ) { 519 $address_line_1 = synoveo_get_mapped_value( 'wordpress-seo', 'addressLines' ); 520 $address_line_2 = $yoast_local['location_address_2'] ?? ''; 521 522 $snapshot['sources']['wordpress-seo'] = array( 523 'title' => synoveo_get_mapped_value( 'wordpress-seo', 'title' ), 524 'storefrontAddress' => array( 525 'addressLines' => array_filter( array( $address_line_1, $address_line_2 ) ), 526 'locality' => synoveo_get_mapped_value( 'wordpress-seo', 'locality' ), 527 'administrativeArea' => synoveo_get_mapped_value( 'wordpress-seo', 'administrativeArea' ), 528 'postalCode' => synoveo_get_mapped_value( 'wordpress-seo', 'postalCode' ), 529 'regionCode' => synoveo_get_mapped_value( 'wordpress-seo', 'regionCode' ), 530 ), 531 'phoneNumbers' => array( 532 'primaryPhone' => synoveo_get_mapped_value( 'wordpress-seo', 'primaryPhone' ), 533 ), 534 'profile' => array( 535 'description' => synoveo_get_mapped_value( 'wordpress-seo', 'profile.description' ), 536 ), 537 'websiteUri' => synoveo_get_mapped_value( 'wordpress-seo', 'websiteUri' ), 538 'socialProfiles' => array_filter( 539 array( 540 'facebook' => synoveo_get_mapped_value( 'wordpress-seo', 'profile.socialMedia.facebook' ), 541 'twitter' => synoveo_get_mapped_value( 'wordpress-seo', 'profile.socialMedia.twitter' ), 542 'instagram' => synoveo_get_mapped_value( 'wordpress-seo', 'profile.socialMedia.instagram' ), 543 'linkedin' => synoveo_get_mapped_value( 'wordpress-seo', 'profile.socialMedia.linkedin' ), 544 'youtube' => synoveo_get_mapped_value( 'wordpress-seo', 'profile.socialMedia.youtube' ), 545 'pinterest' => synoveo_get_mapped_value( 'wordpress-seo', 'profile.socialMedia.pinterest' ), 546 ) 547 ), 548 'categories' => array( 549 'primaryCategory' => synoveo_get_mapped_value( 'wordpress-seo', 'primaryCategory' ), 550 ), 551 ); 552 553 $yoast_hours = synoveo_direct_extract_yoast_hours( $yoast_local ); 554 if ( ! empty( $yoast_hours ) ) { 555 $snapshot['sources']['wordpress-seo']['regularHours'] = $yoast_hours; 556 } 557 } 558 } 559 560 // RankMath SEO - only if active. 561 if ( synoveo_direct_is_plugin_active( 'seo-by-rank-math' ) ) { 562 $rankmath = synoveo_direct_get_option( 'rank-math-options-titles', array() ); 563 if ( ! empty( $rankmath ) ) { 564 $local_address = $rankmath['local_address'] ?? array(); 565 $phone_numbers = $rankmath['phone_numbers'] ?? array(); 566 $primary_phone = ''; 567 if ( ! empty( $phone_numbers ) && is_array( $phone_numbers ) && isset( $phone_numbers[0]['number'] ) ) { 568 $primary_phone = $phone_numbers[0]['number']; 569 } 570 571 $snapshot['sources']['seo-by-rank-math'] = array( 572 'title' => $rankmath['knowledgegraph_name'] ?? $rankmath['website_name'] ?? '', 573 'storefrontAddress' => array( 574 'addressLines' => array_filter( array( $local_address['streetAddress'] ?? '' ) ), 575 'locality' => $local_address['addressLocality'] ?? '', 576 'administrativeArea' => $local_address['addressRegion'] ?? '', 577 'postalCode' => $local_address['postalCode'] ?? '', 578 'regionCode' => $local_address['addressCountry'] ?? '', 579 ), 580 'phoneNumbers' => array( 'primaryPhone' => $primary_phone ), 581 'profile' => array( 'description' => $rankmath['organization_description'] ?? '' ), 582 'websiteUri' => $rankmath['url'] ?? '', 583 'socialProfiles' => array_filter( 584 array( 585 'facebook' => $rankmath['social_url_facebook'] ?? '', 586 'twitter' => $rankmath['twitter_author_names'] ?? '', 587 ) 588 ), 589 'categories' => array( 'primaryCategory' => $rankmath['local_business_type'] ?? '' ), 590 ); 591 592 $rm_opening_hours = $rankmath['opening_hours'] ?? array(); 593 if ( ! empty( $rm_opening_hours ) && is_array( $rm_opening_hours ) ) { 594 $periods = array(); 595 $day_map = array( 596 'monday' => 'MONDAY', 597 'tuesday' => 'TUESDAY', 598 'wednesday' => 'WEDNESDAY', 599 'thursday' => 'THURSDAY', 600 'friday' => 'FRIDAY', 601 'saturday' => 'SATURDAY', 602 'sunday' => 'SUNDAY', 603 ); 604 605 foreach ( $rm_opening_hours as $schedule ) { 606 $day = strtolower( $schedule['day'] ?? '' ); 607 $time = $schedule['time'] ?? ''; 608 609 if ( empty( $day ) || empty( $time ) || ! isset( $day_map[ $day ] ) ) { 610 continue; 611 } 612 613 if ( preg_match( '/(\d{1,2}:\d{2}(?:\s*[AP]M)?)\s*[-–]\s*(\d{1,2}:\d{2}(?:\s*[AP]M)?)/i', $time, $matches ) ) { 614 $open_time = synoveo_time_to_gbp_object( trim( $matches[1] ) ); 615 $close_time = synoveo_time_to_gbp_object( trim( $matches[2] ) ); 616 617 if ( $open_time && $close_time ) { 618 $periods[] = array( 619 'openDay' => $day_map[ $day ], 620 'openTime' => $open_time, 621 'closeDay' => $day_map[ $day ], 622 'closeTime' => $close_time, 623 ); 624 } 625 } 626 } 627 628 if ( ! empty( $periods ) ) { 629 $snapshot['sources']['seo-by-rank-math']['regularHours'] = array( 'periods' => $periods ); 630 } 631 } 632 } 633 } 634 635 // Business Profile plugin - only if active. 636 if ( synoveo_direct_is_plugin_active( 'business-profile' ) ) { 637 $bpfwp = synoveo_direct_get_option( 'bpfwp-settings', array() ); 638 if ( ! empty( $bpfwp ) ) { 639 $snapshot['sources']['business-profile'] = array( 640 'title' => $bpfwp['name'] ?? '', 641 'storefrontAddress' => array( 642 'addressLines' => array_filter( array( $bpfwp['address']['text'] ?? '' ) ), 643 ), 644 'phoneNumbers' => array( 'primaryPhone' => $bpfwp['phone'] ?? '' ), 645 'websiteUri' => $bpfwp['website'] ?? '', 646 ); 647 } 648 } 649 650 // Amelia Booking - only if active. 651 if ( synoveo_direct_is_plugin_active( 'ameliabooking' ) ) { 652 $amelia = synoveo_direct_get_option( 'amelia_settings', '' ); 653 if ( ! empty( $amelia ) ) { 654 $amelia_data = is_string( $amelia ) ? json_decode( $amelia, true ) : $amelia; 655 if ( $amelia_data ) { 656 $amelia_source = array( 657 'title' => $amelia_data['company']['name'] ?? '', 658 'storefrontAddress' => array( 659 'addressLines' => array_filter( array( $amelia_data['company']['address'] ?? '' ) ), 660 ), 661 'phoneNumbers' => array( 'primaryPhone' => $amelia_data['company']['phone'] ?? '' ), 662 'websiteUri' => $amelia_data['company']['website'] ?? '', 663 ); 664 665 // Extract business hours from weekSchedule. 666 if ( ! empty( $amelia_data['weekSchedule'] ) && is_array( $amelia_data['weekSchedule'] ) ) { 667 $periods = array(); 668 foreach ( $amelia_data['weekSchedule'] as $schedule ) { 669 $day = strtoupper( $schedule['day'] ?? '' ); 670 $time = $schedule['time'] ?? array(); 671 672 if ( empty( $time ) || count( $time ) < 2 ) { 673 continue; 674 } 675 676 $periods[] = array( 677 'openDay' => $day, 678 'openTime' => synoveo_time_to_gbp_object( $time[0] ), 679 'closeDay' => $day, 680 'closeTime' => synoveo_time_to_gbp_object( $time[1] ), 681 ); 682 } 683 684 if ( ! empty( $periods ) ) { 685 $amelia_source['regularHours'] = array( 'periods' => $periods ); 686 } 687 } 688 689 $amelia_services = synoveo_direct_get_amelia_services(); 690 if ( $amelia_services ) { 691 $amelia_source['serviceItems'] = $amelia_services; 692 } 693 694 $home_url = rtrim( synoveo_direct_get_option( 'home', '' ), '/' ); 695 $booking_page = $wpdb->get_row( 696 "SELECT ID, post_name FROM {$wpdb->posts} WHERE post_content LIKE '%[ameliabooking%' AND post_status = 'publish' LIMIT 1" 697 ); 698 if ( $booking_page && ! empty( $booking_page->post_name ) ) { 699 $amelia_source['actionLinks'] = array( 'booking' => "{$home_url}/{$booking_page->post_name}/" ); 700 } 701 702 $snapshot['sources']['ameliabooking'] = $amelia_source; 703 } 704 } 705 } 706 707 // AIOSEO (All in One SEO) - only if active. 708 if ( synoveo_direct_is_plugin_active( 'all-in-one-seo-pack' ) ) { 709 $aioseo = synoveo_direct_get_option( 'aioseo_options', '' ); 710 if ( ! empty( $aioseo ) ) { 711 $aioseo_source = array( 712 'title' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'title' ), 713 'storefrontAddress' => array( 714 'addressLines' => array_filter( array( synoveo_get_mapped_value( 'all-in-one-seo-pack', 'addressLines' ) ) ), 715 'locality' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'locality' ), 716 'administrativeArea' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'administrativeArea' ), 717 'postalCode' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'postalCode' ), 718 'regionCode' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'regionCode' ), 719 ), 720 'phoneNumbers' => array( 'primaryPhone' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'primaryPhone' ) ), 721 'profile' => array( 'description' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'profile.description' ) ), 722 'websiteUri' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'websiteUri' ), 723 'socialProfiles' => array_filter( 724 array( 725 'facebook' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'profile.socialMedia.facebook' ), 726 'twitter' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'profile.socialMedia.twitter' ), 727 'instagram' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'profile.socialMedia.instagram' ), 728 'linkedin' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'profile.socialMedia.linkedin' ), 729 'youtube' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'profile.socialMedia.youtube' ), 730 'pinterest' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'profile.socialMedia.pinterest' ), 731 'tiktok' => synoveo_get_mapped_value( 'all-in-one-seo-pack', 'profile.socialMedia.tiktok' ), 732 ) 733 ), 734 ); 735 736 $has_data = ! empty( $aioseo_source['title'] ) || ! empty( $aioseo_source['phoneNumbers']['primaryPhone'] ) || ! empty( $aioseo_source['socialProfiles'] ); 737 if ( $has_data ) { 738 $snapshot['sources']['all-in-one-seo-pack'] = $aioseo_source; 739 } 740 } 741 } 742 743 // Bookly Responsive Appointment Booking - only if active. 744 if ( synoveo_direct_is_plugin_active( 'bookly-responsive-appointment-booking-tool' ) ) { 745 $bookly_monday_start = synoveo_direct_get_option( 'bookly_bh_monday_start', '' ); 746 if ( ! empty( $bookly_monday_start ) ) { 747 $days = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' ); 748 $periods = array(); 749 750 foreach ( $days as $day ) { 751 $start_time = synoveo_direct_get_option( "bookly_bh_{$day}_start", '' ); 752 $end_time = synoveo_direct_get_option( "bookly_bh_{$day}_end", '' ); 753 754 if ( ! empty( $start_time ) && ! empty( $end_time ) ) { 755 $start_time = substr( $start_time, 0, 5 ); 756 $end_time = substr( $end_time, 0, 5 ); 757 758 $periods[] = array( 759 'openDay' => strtoupper( $day ), 760 'openTime' => synoveo_time_to_gbp_object( $start_time ), 761 'closeDay' => strtoupper( $day ), 762 'closeTime' => synoveo_time_to_gbp_object( $end_time ), 763 ); 764 } 765 } 766 767 $bookly_source = array( 768 'regularHours' => ! empty( $periods ) ? array( 'periods' => $periods ) : null, 769 'currencyCode' => synoveo_direct_get_option( 'bookly_pmt_currency', 'USD' ), 770 ); 771 772 $bookly_services = synoveo_direct_get_bookly_services(); 773 if ( $bookly_services ) { 774 $bookly_source['serviceItems'] = $bookly_services; 775 } 776 777 $home_url = rtrim( synoveo_direct_get_option( 'home', '' ), '/' ); 778 $booking_page = $wpdb->get_row( 779 "SELECT ID, post_name FROM {$wpdb->posts} WHERE post_content LIKE '%[bookly-form%' AND post_status = 'publish' LIMIT 1" 780 ); 781 if ( $booking_page && ! empty( $booking_page->post_name ) ) { 782 $bookly_source['actionLinks'] = array( 'booking' => "{$home_url}/{$booking_page->post_name}/" ); 783 } 784 785 $snapshot['sources']['bookly-responsive-appointment-booking-tool'] = $bookly_source; 786 } 787 } 788 789 // Five Star Restaurant Reservations - only if active. 790 if ( synoveo_direct_is_plugin_active( 'five-star-restaurant-reservations' ) ) { 791 $rtb_settings = synoveo_direct_get_option( 'rtb-settings', array() ); 792 if ( ! empty( $rtb_settings ) && is_array( $rtb_settings ) ) { 793 $rtb_data = array( 794 'title' => $rtb_settings['business-name'] ?? '', 795 'phoneNumbers' => array( 'primaryPhone' => $rtb_settings['reply-to-phone'] ?? '' ), 796 ); 797 798 if ( isset( $rtb_settings['schedule-open'] ) && isset( $rtb_settings['schedule-closed'] ) ) { 799 $days = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' ); 800 $periods = array(); 801 802 foreach ( $days as $index => $day ) { 803 $open_times = $rtb_settings['schedule-open'][ $index ] ?? array(); 804 $close_times = $rtb_settings['schedule-closed'][ $index ] ?? array(); 805 806 if ( ! empty( $open_times ) && ! empty( $close_times ) ) { 807 foreach ( $open_times as $slot_index => $open_time ) { 808 $close_time = $close_times[ $slot_index ] ?? ''; 809 if ( ! empty( $open_time ) && ! empty( $close_time ) ) { 810 $periods[] = array( 811 'openDay' => strtoupper( $day ), 812 'openTime' => synoveo_time_to_gbp_object( $open_time ), 813 'closeDay' => strtoupper( $day ), 814 'closeTime' => synoveo_time_to_gbp_object( $close_time ), 815 ); 816 } 817 } 818 } 819 } 820 821 if ( ! empty( $periods ) ) { 822 $rtb_data['regularHours'] = array( 'periods' => $periods ); 823 } 824 } 825 826 if ( ! empty( $rtb_data['title'] ) || ! empty( $rtb_data['phoneNumbers']['primaryPhone'] ) || isset( $rtb_data['regularHours'] ) ) { 827 $snapshot['sources']['five-star-restaurant-reservations'] = $rtb_data; 828 } 829 } 830 } 831 832 // MotoPress Hotel Booking (Lodging) - only if active. 833 if ( synoveo_direct_is_plugin_active( 'motopress-hotel-booking-lite' ) ) { 834 $mphb_settings = synoveo_direct_get_option( 'mphb_settings', array() ); 835 if ( ! empty( $mphb_settings ) && is_array( $mphb_settings ) ) { 836 $lodging_data = array(); 837 838 if ( isset( $mphb_settings['general'] ) ) { 839 $lodging_data['title'] = $mphb_settings['general']['business_name'] ?? ''; 840 } 841 842 if ( isset( $mphb_settings['currency'] ) ) { 843 $lodging_data['currencyCode'] = $mphb_settings['currency']['currency_symbol'] ?? ''; 844 } 845 846 if ( isset( $mphb_settings['booking'] ) ) { 847 $lodging_data['lodging'] = array( 848 'policies' => array( 849 'checkInTime' => $mphb_settings['booking']['check_in_time'] ?? '', 850 'checkOutTime' => $mphb_settings['booking']['check_out_time'] ?? '', 851 ), 852 ); 853 } 854 855 if ( ! empty( $lodging_data['title'] ) || isset( $lodging_data['lodging'] ) ) { 856 $snapshot['sources']['motopress-hotel-booking-lite'] = $lodging_data; 857 } 858 } 859 } 860 861 // Five Star Restaurant Menu (fdm plugin) - only if active. 862 if ( synoveo_direct_is_plugin_active( 'food-and-drink-menu' ) ) { 863 $fdm_settings = synoveo_direct_get_option( 'fdm-settings', array() ); 864 if ( ! empty( $fdm_settings ) && is_array( $fdm_settings ) ) { 865 $menu_data = array(); 866 867 if ( isset( $fdm_settings['business-name'] ) ) { 868 $menu_data['title'] = $fdm_settings['business-name']; 869 } 870 871 if ( isset( $fdm_settings['currency-symbol'] ) ) { 872 $menu_data['currencyCode'] = $fdm_settings['currency-symbol']; 873 } 874 875 if ( ! empty( $menu_data ) ) { 876 $snapshot['sources']['food-and-drink-menu'] = $menu_data; 877 } 878 } 879 } 1037 'version' => '2.0', 1038 'timestamp' => gmdate( 'c' ), 1039 'sources' => array(), 1040 ); 1041 1042 // Dynamically detect active plugins from registry. 1043 // No hardcoding - add new plugin = add mapping file. 1044 $active_plugins = synoveo_get_active_plugin_keys(); 1045 1046 // Export using modular mappings with batch DB read + transforms. 1047 $snapshot['sources'] = synoveo_export_snapshot_sources( $wpdb, $active_plugins ); 880 1048 881 1049 return $snapshot; … … 922 1090 923 1091 // Get visible services. 924 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SHORTINIT mode requires direct queries. 1092 // Note: Table name comes from our own config (not user input), so safe to interpolate. 1093 // Using backticks directly because %i placeholder may not work in SHORTINIT mode. 1094 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- SHORTINIT mode requires direct queries. 925 1095 $services = $wpdb->get_results( 926 1096 $wpdb->prepare( 927 'SELECT id, name, description, price, duration FROM %i WHERE status = %s ORDER BY position ASC LIMIT 50', 928 $table, 1097 "SELECT id, name, description, price, duration FROM `{$table}` WHERE status = %s ORDER BY position ASC LIMIT 50", 929 1098 'visible' 930 1099 ), … … 991 1160 992 1161 // Get public services. 993 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SHORTINIT mode requires direct queries. 1162 // Note: Table name comes from our own config (not user input), so safe to interpolate. 1163 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- SHORTINIT mode requires direct queries. 994 1164 $services = $wpdb->get_results( 995 1165 $wpdb->prepare( 996 'SELECT id, title, info, price, duration FROM %i WHERE visibility = %s ORDER BY position ASC LIMIT 50', 997 $table, 1166 "SELECT id, title, info, price, duration FROM `{$table}` WHERE visibility = %s ORDER BY position ASC LIMIT 50", 998 1167 'public' 999 1168 ), … … 1048 1217 1049 1218 // Get products (limit to 50 for performance). 1219 // Note: Table names come from wpdb properties (WordPress core), so safe to use. 1050 1220 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SHORTINIT mode requires direct queries. 1051 $products = $wpdb->get_results( 1052 $wpdb->prepare( 1053 "SELECT p.ID, p.post_title, p.post_excerpt, pm.meta_value as price 1054 FROM %i p 1055 LEFT JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_regular_price' 1056 WHERE p.post_type = 'product' AND p.post_status = 'publish' 1057 ORDER BY p.menu_order ASC, p.post_date DESC 1058 LIMIT 50", 1059 $wpdb->posts, 1060 $wpdb->postmeta 1061 ), 1221 $posts_table = $wpdb->posts; 1222 $postmeta_table = $wpdb->postmeta; 1223 $products = $wpdb->get_results( 1224 "SELECT p.ID, p.post_title, p.post_excerpt, pm.meta_value as price 1225 FROM `{$posts_table}` p 1226 LEFT JOIN `{$postmeta_table}` pm ON p.ID = pm.post_id AND pm.meta_key = '_regular_price' 1227 WHERE p.post_type = 'product' AND p.post_status = 'publish' 1228 ORDER BY p.menu_order ASC, p.post_date DESC 1229 LIMIT 50", 1062 1230 ARRAY_A 1063 1231 ); … … 1212 1380 $path = $field_paths[ $field_name ] ?? $field_name; 1213 1381 1214 // Navigate nested path. 1215 $value = $source; 1216 foreach ( explode( '.', $path ) as $key ) { 1217 if ( is_array( $value ) && isset( $value[ $key ] ) ) { 1218 $value = $value[ $key ]; 1219 } else { 1220 return array( 1221 'found' => false, 1222 'value' => null, 1223 'reason' => 'field_not_found', 1224 ); 1382 // First, try direct key lookup (snapshot uses flat keys like "phoneNumbers.primaryPhone"). 1383 if ( isset( $source[ $path ] ) ) { 1384 $value = $source[ $path ]; 1385 } elseif ( isset( $source[ $field_name ] ) ) { 1386 // Try original field name if path mapping didn't match. 1387 $value = $source[ $field_name ]; 1388 } else { 1389 // Fallback: Navigate nested path for backwards compatibility. 1390 $value = $source; 1391 foreach ( explode( '.', $path ) as $key ) { 1392 if ( is_array( $value ) && isset( $value[ $key ] ) ) { 1393 $value = $value[ $key ]; 1394 } else { 1395 return array( 1396 'found' => false, 1397 'value' => null, 1398 'reason' => 'field_not_found', 1399 ); 1400 } 1225 1401 } 1226 1402 } … … 1292 1468 } 1293 1469 1470 // Import request (POST only). 1471 if ( '/synoveo/v1/import' === $route ) { 1472 // Verify POST method. 1473 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Checking request method only. 1474 $method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( $_SERVER['REQUEST_METHOD'] ) : 'GET'; 1475 if ( 'POST' !== $method ) { 1476 http_response_code( 405 ); 1477 header( 'Allow: POST' ); 1478 // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- SHORTINIT mode. 1479 echo json_encode( 1480 array( 1481 'success' => false, 1482 'error' => 'Method not allowed. Use POST.', 1483 ) 1484 ); 1485 exit; 1486 } 1487 1488 $result = synoveo_handle_import(); 1489 $elapsed = round( ( microtime( true ) - $start ) * 1000, 2 ); 1490 1491 $status_code = $result['success'] ? 200 : 400; 1492 http_response_code( $status_code ); 1493 1494 // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- SHORTINIT mode. 1495 echo json_encode( 1496 array_merge( 1497 $result, 1498 array( 1499 'meta' => array( 1500 'import_time_ms' => $elapsed, 1501 'method' => 'direct-db', 1502 ), 1503 ) 1504 ), 1505 JSON_PRETTY_PRINT 1506 ); 1507 exit; 1508 } 1509 1510 // Debug request. 1511 if ( '/synoveo/v1/debug' === $route ) { 1512 $debug_info = synoveo_get_debug_info(); 1513 $elapsed = round( ( microtime( true ) - $start ) * 1000, 2 ); 1514 1515 // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode -- SHORTINIT mode. 1516 echo json_encode( 1517 array( 1518 'success' => true, 1519 'data' => $debug_info, 1520 'meta' => array( 1521 'debug_time_ms' => $elapsed, 1522 'method' => 'direct-db', 1523 ), 1524 ), 1525 JSON_PRETTY_PRINT 1526 ); 1527 exit; 1528 } 1529 1294 1530 // Fallback. 1295 1531 http_response_code( 404 ); -
synoveo/trunk/includes/rest/handlers/class-synoveo-business-data-handler.php
r3429757 r3433224 234 234 // Dashboard uses these keys directly via composite key: {plugin}_{gbp_field}. 235 235 // See: docs/wordpress/singlePluginSource.md. 236 // 237 // IMPORTANT: Include BOTH short and full GBP paths for compatibility. 238 // - Short: 'primaryPhone' (for legacy/internal use). 239 // - Full: 'phoneNumbers.primaryPhone' (for delta comparison view). 236 240 $export_fields = array( 237 241 'title', 238 242 'primaryPhone', 243 'phoneNumbers.primaryPhone', // Full GBP path for delta comparison. 239 244 'email', 240 245 'profile.description', 246 'websiteUri', 241 247 'addressLines', 242 248 'locality', -
synoveo/trunk/includes/rest/handlers/class-synoveo-capabilities-handler.php
r3429757 r3433224 199 199 ), 200 200 ), 201 // Social Media fields ( mapped to GBP attributes).201 // Social Media fields (using internal profile.socialMedia naming). 202 202 'attributes/url_facebook' => array( 203 203 array( … … 234 234 ), 235 235 array( 236 'plugin' => 'seo-by-rank-math',237 'display_name' => esc_html__( 'Rank Math (Instagram)', 'synoveo' ),238 ),239 array(240 236 'plugin' => 'all-in-one-seo-pack', 241 237 'display_name' => esc_html__( 'All in One SEO (Instagram)', 'synoveo' ), … … 248 244 ), 249 245 array( 250 'plugin' => 'seo-by-rank-math',251 'display_name' => esc_html__( 'Rank Math (LinkedIn)', 'synoveo' ),252 ),253 array(254 246 'plugin' => 'all-in-one-seo-pack', 255 247 'display_name' => esc_html__( 'All in One SEO (LinkedIn)', 'synoveo' ), … … 262 254 ), 263 255 array( 264 'plugin' => 'seo-by-rank-math',265 'display_name' => esc_html__( 'Rank Math (YouTube)', 'synoveo' ),266 ),267 array(268 256 'plugin' => 'all-in-one-seo-pack', 269 257 'display_name' => esc_html__( 'All in One SEO (YouTube)', 'synoveo' ), … … 276 264 ), 277 265 array( 278 'plugin' => 'seo-by-rank-math',279 'display_name' => esc_html__( 'Rank Math (Pinterest)', 'synoveo' ),280 ),281 array(282 266 'plugin' => 'all-in-one-seo-pack', 283 267 'display_name' => esc_html__( 'All in One SEO (Pinterest)', 'synoveo' ), … … 286 270 'attributes/url_tiktok' => array( 287 271 array( 288 'plugin' => 'wordpress-seo',289 'display_name' => esc_html__( 'Yoast SEO (TikTok)', 'synoveo' ),290 ),291 array(292 'plugin' => 'seo-by-rank-math',293 'display_name' => esc_html__( 'Rank Math (TikTok)', 'synoveo' ),294 ),295 array(296 272 'plugin' => 'all-in-one-seo-pack', 297 273 'display_name' => esc_html__( 'All in One SEO (TikTok)', 'synoveo' ), … … 300 276 'attributes/url_whatsapp' => array( 301 277 array( 302 'plugin' => 'wordpress-seo', 303 'display_name' => esc_html__( 'Yoast SEO (WhatsApp)', 'synoveo' ), 304 ), 305 array( 306 'plugin' => 'seo-by-rank-math', 307 'display_name' => esc_html__( 'Rank Math (WhatsApp)', 'synoveo' ), 308 ), 309 array( 310 'plugin' => 'all-in-one-seo-pack', 311 'display_name' => esc_html__( 'All in One SEO (WhatsApp)', 'synoveo' ), 312 ), 313 ), 278 'plugin' => 'synoveo', 279 'display_name' => esc_html__( 'Synoveo (WhatsApp)', 'synoveo' ), 280 ), 281 ), 282 // WhatsApp: No WordPress plugin supports this - only in GBP attributes 314 283 // Service Items - GBP API field: locations.serviceItems[]. 315 284 // Contains structuredServiceItem (Google predefined) or freeFormServiceItem (custom). -
synoveo/trunk/includes/services/class-synoveo-option-mapping.php
r3429757 r3433224 128 128 'phoneNumbers.primaryPhone' => 'aioseo_options:searchAppearance.global.schema.phone', 129 129 'profile.description' => 'aioseo_options:searchAppearance.global.schema.organizationDescription', 130 'websiteUri' => ' siteurl',130 'websiteUri' => 'aioseo_options:searchAppearance.global.schema.url', 131 131 'email' => 'aioseo_options:searchAppearance.global.schema.email', 132 132 … … 270 270 // No WordPress plugin stores these. Synoveo is the only option. 271 271 'attributes' => 'synoveo_attributes', 272 'attributes/url_whatsapp' => 'synoveo_whatsapp_url', 272 273 273 274 // Service Area - for service-area businesses (plumbers, delivery, etc.). -
synoveo/trunk/readme.txt
r3431468 r3433224 1 === Synoveo – Business Profile Sync for Google===2 Contributors: synoveo1 === Synoveo – Control Your Google Maps Listing === 2 Contributors: Synoveo 3 3 Donate link: https://www.synoveo.com 4 Tags: google , reviews, local-seo, woocommerce, schema4 Tags: google my business, google maps, booking, local seo, schema 5 5 Requires at least: 6.2 6 6 Tested up to: 6.9 7 7 Requires PHP: 7.4 8 Stable tag: 2. 2.28 Stable tag: 2.3.1 9 9 License: GPLv2 or later 10 10 11 Display Google reviews and sync your business info to Google Business Profile — automatically. 11 Your customers search Google Maps to find you. But what does Google show them? 12 12 13 13 == Description == 14 14 15 Display Google reviews on your WordPress site and keep your Google Business Profile in sync — automatically.15 If you don't tell Google who you are, Google decides for you. Using outdated info, Booking.com listings, or Airbnb pages instead of YOUR website. 16 16 17 **Synoveo** connects your WordPress site to your Google Business Profile, making it easy to show reviews, post updates, and maintain accurate business information.17 Synoveo makes your WordPress site the source of truth. You write your info once. Google Maps shows it. 18 18 19 = What It Does=19 = The Problem = 20 20 21 * Display Google Business Profile reviews with shortcodes 22 * Auto-post WordPress content to Google Business Profile (Pro) 23 * Keep business info synced between WordPress and Google 24 * Inject Schema.org structured data for better SEO (Pro) 25 * Works with WooCommerce, RankMath, Yoast, and more 21 Google needs information about your business. If you don't provide it clearly, Google fills in the gaps: 26 22 27 = Pricing Plans = 23 * Wrong hours from an old directory 24 * A description Google wrote for you 25 * Links to Booking.com or Airbnb instead of your website 26 * Phone numbers that don't work 28 27 29 **FREE ($0/month)** – Perfect for getting started 30 * Reviews & rating display (with Synoveo branding) 31 * Manual refresh (1 per day) 28 Your customers get confused. They book somewhere else. They show up when you're closed. They blame you. 29 30 = The Solution = 31 32 Synoveo connects your WordPress site to Google Business Profile (formerly Google My Business). 33 34 You write your business information once on your own website. Synoveo sends it to Google. Google Maps shows what YOU said. 35 36 No more guessing. No more outdated info. No more sending customers to Booking.com or Expedia when you have your own booking system. 37 38 = The Result = 39 40 * Google Maps shows YOUR hours, YOUR description, YOUR links 41 * Customers go to YOUR website, not third party sites 42 * You control what Google says about your business 43 44 **One sentence:** Synoveo makes sure Google repeats what's on your website instead of deciding for you. 45 46 = What You Get = 47 48 **FREE** Try it, see it work 49 50 * See your real Google reviews on your website 51 * Update Google once a day with one click 32 52 * 1 location 33 53 34 **PRO ($9.90/month or $99/year — save 17%)** – Full automation 35 * Automatic daily sync 36 * Auto-post WordPress content to Google Business Profile 37 * Schema.org structured data injection 38 * No branding on reviews 39 * Plugin integrations (RankMath, Yoast, WooCommerce, etc.) 40 * Additional locations: +$5/month each 54 **PRO $9.90/month** Set it and forget it 41 55 42 = How It Works = 43 44 1. Install the plugin 45 2. Create your account at app.synoveo.com 46 3. Enter your API credentials 47 4. Connect your Google Business Profile 48 5. Reviews appear automatically — Pro users get daily sync 49 50 **All the intelligence lives in the Synoveo platform** – no complex configuration needed. 51 52 = Requirements = 53 54 * WordPress 6.2 or higher 55 * PHP 7.4 or higher (compatible with PHP 7.4, 8.0, 8.1, 8.2, 8.3, and 8.4) 56 * Synoveo account (free plan available) 57 * HTTPS enabled (for secure API communication) 56 * Google always has your latest info. You do nothing. 57 * Announce news, offers, events on Google Maps from WordPress 58 * Google understands your business better. You rank higher. 59 * Your site looks professional. No Synoveo branding. 60 * Multiple locations? Add $5/month each 61 * Pay yearly, save 17% 58 62 59 63 == Installation == 60 64 61 1. Upload the plugin files to `/wp-content/plugins/synoveo/` or install directly from WordPress plugin search 62 2. Activate the plugin through the 'Plugins' screen 63 3. Go to Synoveo menu in WordPress admin 64 4. Click "Open Synoveo Hub" to create your account at app.synoveo.com (use the same Gmail as your Google Business Profile) 65 5. Choose a plan (Free for manual usage, Pro for full automation) 66 6. In the Synoveo Hub, go to "API Keys" and generate credentials 67 7. Copy your Client ID and Client Secret back to the plugin 68 8. Click "Connect" to link your site 65 1. Install Synoveo from the WordPress plugin directory 66 2. Create your free account at app.synoveo.com (use the same email as your Google Business Profile) 67 3. Copy your credentials into the plugin 68 4. Done. Google now gets your info from your website. 69 70 No technical setup. No field mapping. The plugin handles everything. 69 71 70 72 == Frequently Asked Questions == 71 73 72 = Wh ere do I get my credentials? =74 = Why do I need this? = 73 75 74 Log in to the Synoveo Hub at https://app.synoveo.com and go to the "API Keys" section to generate your Client ID and Client Secret.76 When customers search Google Maps for your business, Google shows them information. If you don't control that information, Google uses whatever it finds: old directories, Booking.com, Airbnb, or its own guesses. Synoveo makes sure Google shows what YOU say. 75 77 76 = What data does this plugin send? =78 = What is Google Business Profile? = 77 79 78 The plugin sends basic WordPress information (site name, URL, description, etc.) and WooCommerce data if installed (business hours, categories). All data is sent securely via HTTPS to the Synoveo platform where it's transformed to Google Business Profile format.80 It is how your business appears on Google Maps and Google Search. You may know it as Google My Business. Same thing, new name. 79 81 80 = Do I need to configure field mappings? =82 = I already have a Google Business Profile. Why do I need Synoveo? = 81 83 82 No! The Synoveo platform handles all field mapping and transformation. This plugin just sends raw WordPress data.84 You can update Google manually. But your WordPress site already has your hours, your description, your links. Why type it twice? Synoveo sends your WordPress info to Google automatically. One source of truth. 83 85 84 = Is my data secure? =86 = Will this help me rank higher on Google Maps? = 85 87 86 Yes. All communication uses HTTPS and JWT authentication. Your credentials are stored securely in WordPress options and encrypted tokens are used for API communication.88 Yes. Google rewards businesses with complete, accurate, consistent information. When your website and Google Business Profile match perfectly, Google trusts you more. 87 89 88 = What PHP versions are supported? =90 = Do I need technical skills? = 89 91 90 The plugin supports PHP 7.4 and all PHP 8.x versions (8.0, 8.1, 8.2, 8.3, 8.4). We include polyfills for PHP 8.0+ functions to ensure compatibility with older hosts.92 No. Install the plugin, create an account, paste your credentials. That is all. 91 93 92 = What happens to my data if I deactivate the plugin? =94 = What if I have multiple locations? = 93 95 94 When you deactivate the plugin, you'll be asked if you want to keep or delete your Synoveo data. If you choose to keep it, your settings will be preserved if you reinstall later. If you choose to delete, all credentials and settings will be removed when you delete the plugin.96 Free plan includes 1 location. Pro plan lets you add more at $5/month each. 95 97 96 = Can I use multiple WordPress sites with one Synoveo account? =98 = Does it work with my booking system? = 97 99 98 Yes! Each WordPress site needs its own API key from the Synoveo Hub. Your plan determines how many locations you can sync across all your sites.100 Synoveo works with WooCommerce and most WordPress booking plugins. Your booking links go to YOUR system, not to Booking.com or Expedia. 99 101 100 = Does this work with WooCommerce? =102 = What about my Google reviews? = 101 103 102 Yes, the plugin automatically detects WooCommerce and can sync relevant business information like store hours, categories, and product data to enhance your Google Business Profile.104 Synoveo displays your Google reviews on your WordPress site. Show visitors what real customers say without them leaving your page. 103 105 104 = How often does the plugin sync data? =106 = Is my data safe? = 105 107 106 Free users can manually refresh once per day. Pro users get automatic daily synchronization managed by the Synoveo platform, plus unlimited manual refreshes. 107 108 = What's the difference between Free and Pro? = 109 110 **Free** gives you reviews display with Synoveo branding, manual refresh (limited to 1/day), and 1 location. **Pro** ($9.90/month) removes branding, enables automatic daily sync, auto-posting to Google Business Profile, Schema.org injection, and plugin integrations. Save 17% with annual billing ($99/year). 111 112 = What if I have issues connecting? = 113 114 Check that your site has HTTPS enabled and your credentials are correct. Visit the Synoveo Hub for connection status and logs. Contact support@synoveo.com if you need help. 108 Yes. All communication is encrypted. Your Google credentials never touch our servers. We only send your public business information to Google. 115 109 116 110 == Screenshots == 117 111 118 1. Dashboard – Connect your WordPress site to Synoveo platform119 2. Settings – Configure your API credentials and sync options120 3. Auto-Post (Pro) – Automatically publish WordPress posts to Google Business Profile121 4. Reviews – Display Google reviews on your site with shortcodes112 1. Dashboard. See your connection status and Google reviews at a glance. 113 2. Your Google reviews displayed on your website. Real customers, real trust. 114 3. Post updates to Google Maps directly from WordPress. Announce offers, news, events. 115 4. Settings. Connect once, works forever. 122 116 123 117 == Changelog == 124 118 119 = 2.3.1 = 120 * Improved reliability for all plugin integrations (AIOSEO, RankMath, Yoast, WooCommerce, Bookly, Amelia) 121 * Better sync between Google and WordPress 122 123 = 2.3.0 = 124 * 16x faster responses 125 * Two-way sync. Google data can now update your WordPress site. 126 * Support for Bookly and Amelia booking plugins 127 125 128 = 2.2.2 = 126 * Fixed Schema.org review replies using correct `comment` property instead of non-standard `replyToUrl` 127 * Added smart fallbacks for `priceRange` and `servesCuisine` covering all 112 GBP business types 128 * Eliminates Google Rich Results validation warnings for missing optional fields 129 * Fixed Google reconnect notification to redirect to plugin dashboard instead of external URL 130 * Fixed Pro plan branding detection - Pro users no longer see "Powered by Synoveo" on reviews 131 * Added plan validation with allowlist (security hardening) 132 * Business plan users now correctly excluded from branding 133 * Improved Schema.org LocalBusiness structured data compliance for better SEO 134 135 = 2.2.1 = 136 * Added manual refresh button for Free plan users on Reviews & Ratings page 137 * Refresh syncs reviews, ratings, and business info from Google 138 * Button shows quota status (1 refresh/day for Free, unlimited for Pro) 139 * Automatic token refresh for reliable Google API connections 140 141 = 2.2.0 = 142 * Simplified pricing: Free and Pro plans (replaces previous 4-tier model) 143 * Free plan: Reviews display with branding, manual refresh (1/day limit) 144 * Pro plan: Automatic daily sync, auto-post, Schema.org, no branding ($9.90/mo or $99/yr) 145 * Added "Powered by Synoveo" branding for Free plan users 146 * Rate-limited manual refresh for Free users (1 per day) 147 * Pro users get unlimited manual refreshes 148 * Additional locations available as add-on ($5/month each) 149 * Updated admin UI with clearer plan upgrade prompts 150 * Improved plan-awareness trait for consistent feature gating 151 152 = 2.1.0 = 153 * Added deactivation feedback modal for better user experience 154 * Data deletion choice integrated into deactivation flow 155 * Improved uninstall cleanup with user preference 156 * WordPress coding standards compliance updates 157 158 = 2.0.8 = 159 * Auto-post feature for automatic Google Business Profile posts 160 * Gutenberg sidebar integration for post scheduling 161 * REST API enhancements for post management 162 163 = 2.0.4 = 164 * Minimum WordPress version updated to 6.2 (for %i identifier placeholder support) 165 * Verified PHP 7.4 – 8.4 compatibility 166 * WordPress 6.9 compatibility 167 * PHPCS WordPress coding standards compliance 168 * Security hardening for REST API endpoints 169 170 = 2.0.0 = 171 * Complete refactor to bridge architecture 172 * Streamlined codebase with improved modularity 173 * Enhanced analyzer/detector integration with platform 174 * Improved admin interface 175 * Faster and more reliable 176 177 = 1.2.4 = 178 * Legacy version with complex architecture 129 * Better SEO. Google understands your reviews more clearly. 130 * Fixed branding for Pro users 179 131 180 132 == Upgrade Notice == 181 133 134 = 2.3.1 = 135 Improved reliability for keeping your Google info in sync. 136 137 = 2.3.0 = 138 Major speed improvement. Your site responds 16x faster when talking to Google. 139 182 140 = 2.2.2 = 183 Schema.org structured data now uses the correct `comment` property for business review replies, improving SEO compliance. 184 185 = 2.2.1 = 186 Free plan users can now manually refresh reviews and ratings directly from the WordPress admin. Find the refresh button on the Reviews & Ratings page. 187 188 = 2.2.0 = 189 Simplified pricing! Now just Free and Pro plans. Free includes reviews with branding and 1 manual refresh per day. Pro ($9.90/mo) adds automation, Schema.org, and removes branding. Upgrade at app.synoveo.com/dashboard/billing. 190 191 = 2.1.0 = 192 Improved deactivation experience with feedback collection and data deletion choice. 193 194 = 2.0.4 = 195 Requires WordPress 6.2+. PHP 7.4-8.4 compatibility verified. WordPress 6.9 tested. Security improvements. 196 197 = 2.0.0 = 198 Major simplification! The plugin is now just a simple bridge. Your existing credentials will continue to work. 141 Better SEO. Google now understands your business reviews more clearly. -
synoveo/trunk/synoveo.php
r3431468 r3433224 1 1 <?php 2 2 /** 3 * Plugin Name: Synoveo – Business Profile Sync for Google3 * Plugin Name: Synoveo – Control Your Google Maps Listing 4 4 * Plugin URI: https://www.synoveo.com 5 * Description: Con nect Google Business Profile to WordPress. Sync posts, display reviews and ratings, and expose structured business data on your site. Compatible with Yoast, Rank Math, AIOSEO, and WooCommerce.6 * Version: 2. 2.25 * Description: Control what Google Maps shows about your business. Your WordPress site becomes the source of truth. Display reviews, post updates, stop Google from guessing. 6 * Version: 2.3.1 7 7 * Author: Synoveo 8 8 * License: GPL v2 or later … … 20 20 21 21 // Plugin constants. 22 define( 'SYNOVEO_VERSION', '2. 2.2' );22 define( 'SYNOVEO_VERSION', '2.3.1' ); 23 23 define( 'SYNOVEO_PLUGIN_FILE', __FILE__ ); 24 24 define( 'SYNOVEO_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
Note: See TracChangeset
for help on using the changeset viewer.