er_Manager', 'update_collection_name')); add_action('wp_ajax_sbr_feed_saver_manager_get_source_posts', array('SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager', 'get_source_posts')); add_action('wp_ajax_sbr_feed_saver_manager_delete_review_from_collection', array('SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager', 'delete_review_from_collection')); add_action('wp_ajax_sbr_feed_saver_manager_add_multiple_reviews_collection', array('SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager', 'add_multiple_reviews_collection')); add_action('wp_ajax_sbr_feed_saver_manager_advanced_search_reviews', array('SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager', 'advanced_search_reviews')); add_action('wp_ajax_sbr_feed_saver_manager_duplicate_collection', array('SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager', 'duplicate_collection')); add_action('wp_ajax_sbr_feed_saver_manager_load_more_sources', array('SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager', 'load_more_sources')); add_action('wp_ajax_sbr_feed_saver_manager_export_collection', array('SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager', 'export_collection')); add_action('wp_ajax_sbr_import_full_collection', array('SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager', 'import_full_collection')); add_action('wp_ajax_sbr_import_reviews_collection', array('SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager', 'import_reviews_collection')); add_action('wp_ajax_sbr_clear_error_logs', array('SmashBalloon\Reviews\Common\Helpers\SBR_Error_Handler', 'clear_all_error_ajax')); // After the post-clear background cron refresh repopulates wp_sbr_feed_caches, // re-flush 3rd-party page caches so any HTML they baked during the warm-up // window (when feed cache was empty) is evicted. See flush_third_party_caches(). add_action('sbr_after_cron_refresh', array('SmashBalloon\Reviews\Common\Builder\SBR_Feed_Saver_Manager', 'flush_third_party_caches')); } /** * Used in an AJAX call to update settings for a particular feed. * Can also be used to create a new feed if no feed_id sent in * $_POST data. * * @since 1.0 */ public static function builder_update() { check_ajax_referer('sbr-admin', 'nonce'); if (! sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } $settings_data = $_POST; $feed_id = false; $is_new_feed = isset($settings_data['new_insert']) ? true : false; if (!empty($settings_data['feed_id'])) { $feed_id = sanitize_text_field($settings_data['feed_id']); unset($settings_data['feed_id']); } elseif (isset($settings_data['feed_id'])) { unset($settings_data['feed_id']); } unset($settings_data['action']); unset($settings_data['nonce']); if (!isset($settings_data['feed_name'])) { $settings_data['feed_name'] = ''; } $update_feed = isset($settings_data['update_feed']) ? true : false; unset($settings_data['update_feed']); $settings_data = self::sanitize_settings($settings_data); //Check if New if ($is_new_feed) { $settings_data['sources'] = json_decode(stripslashes($settings_data['sources'])); $is_collection = false; foreach ($settings_data['sources'] as $s_source) { if (!is_array($s_source) && strpos($s_source, 'collection') !== false) { $is_collection = true; } } if ($is_collection) { $settings_data['filterCharCountMin'] = 0; } $new_name = self::create_feed_name(stripslashes($settings_data['feed_name'])); $settings_data['feed_name'] = !empty($new_name) ? $new_name : 'Reviews Feed'; $settings_data = SBR_Feed_Saver_Manager::get_feed_settings_by_feed_templates($settings_data); } unset($settings_data['new_insert']); $feed_name = ''; $feed_style = ''; if ($update_feed) { $feed_name = stripslashes($settings_data['feed_name']); $feed_style = stripslashes($settings_data['feed_style']); $settings_data = json_decode(stripslashes($settings_data['settings']), true); } $feed_saver = new SBR_Feed_Saver($feed_id); $feed_saver->set_feed_name($feed_name); $feed_saver->set_feed_style($feed_style); $feed_saver->set_data($settings_data); $return = array( 'success' => false, 'feed_id' => false, ); if ($feed_saver->update_or_insert()) { $return = array( 'success' => true, 'feed_id' => $feed_saver->get_feed_id(), ); if ($is_new_feed) { echo wp_json_encode($return); wp_die(); } else { $feed_cache = new FeedCache($feed_id); if (isset($_POST['get_posts']) && $_POST['get_posts'] == true) { $feed = Util::sbr_is_pro() ? new \SmashBalloon\Reviews\Pro\Feed($settings_data, $feed_id, $feed_cache) : new \SmashBalloon\Reviews\Common\Feed($settings_data, $feed_id, $feed_cache); $feed->init(); $feed->get_set_cache(); $posts = $feed->get_post_set_page(); $return['posts'] = $posts; } } } echo wp_json_encode($return); wp_die(); } /** * Create Feed Name * This will create the feed name when creating new Feeds * * @since 1.0 */ public static function create_feed_name($feed_name) { return DB::feeds_query_name($feed_name); } /** * Used in an AJAX call to delete feeds from the Database * $_POST data. * * @since 1.0 */ public static function delete_feed() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } $feeds_id = json_decode(stripslashes($_POST['feeds_ids'])); if (! empty($feeds_id) && is_array($feeds_id)) { $feeds_id = array_map('absint', $feeds_id); DB::delete_feeds_query($feeds_id); } } /** * Used in an AJAX call to duplicate a Feed * $_POST data. * * @since 1.0 */ public static function duplicate_feed() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (! empty($_POST['feed_id'])) { $feed_id = absint($_POST['feed_id']); DB::duplicate_feed_query($feed_id); } } /** * Used to retrieve Feed Posts for preview screen * Returns Feed info or false! * * * * @since 1.0 */ public static function feed_customizer_fly_preview() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (isset($_POST['feedID']) && isset($_POST['previewSettings'])) { $return = [ 'posts' => [] ]; $preview_settings = json_decode(stripslashes($_POST['previewSettings']), true); $preview_settings = self::sanitize_settings($preview_settings); $feed_id = absint($_POST['feedID']); $feed_cache = new FeedCache($feed_id, 30000); $feed_cache->clear('posts'); $feed_cache->clear('header'); $feed = Util::sbr_is_pro() ? new \SmashBalloon\Reviews\Pro\Feed($preview_settings, $feed_id, $feed_cache) : new \SmashBalloon\Reviews\Common\Feed($preview_settings, $feed_id, $feed_cache); $feed->init(); $feed->get_set_cache(); $posts = $feed->get_post_set_page(); $feed_id = absint($_POST['feedID']); $feed_name = sanitize_text_field(wp_unslash($_POST['feedName'])); if (isset($posts['data'])) { $posts = $posts['data']; } if (isset($preview_settings['sortRandomEnabled']) && $preview_settings['sortRandomEnabled'] === true) { shuffle($posts); } // Array of Included Stars $preview_settings['includedStarFilters'] $feed_saver = new SBR_Feed_Saver($feed_id); $feed_saver->set_feed_name($feed_name); $feed_saver->set_data($preview_settings); // Update feed settings depending on feed templates if (isset($_POST['isFeedTemplatesPopup'])) { $preview_settings = SBR_Feed_Saver_Manager::get_feed_settings_by_feed_templates($preview_settings); $return['settings'] = $preview_settings; } $return['posts'] = $posts; $return['sourcesList'] = SBR_Sources::get_sources_list([ 'id' => !empty($preview_settings['sources']) && isset($preview_settings['sources']) ? $preview_settings['sources'] : [], ]); echo sbr_json_encode( $return ); } wp_die(); } /** * Used to Start the Moderation Mode & retrieve Reviews * * * * @since 1.0 */ public static function start_moderation_mode() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (isset($_POST['feedID']) && isset($_POST['previewSettings'])) { $preview_settings = json_decode(stripslashes($_POST['previewSettings']), true); $preview_settings = self::sanitize_settings($preview_settings); $feed_id = absint($_POST['feedID']); $preview_settings['numPostDesktop'] = 100; $preview_settings['numPostTablet'] = 100; $preview_settings['numPostMobile'] = 100; $feed = Util::sbr_is_pro() ? new \SmashBalloon\Reviews\Pro\Feed($preview_settings, $feed_id, new FeedCache($feed_id, 30000)) : new \SmashBalloon\Reviews\Common\Feed($preview_settings, $feed_id, new FeedCache($feed_id, 30000)); $feed->init(); $posts = $feed->get_posts_for_moderation(); echo sbr_json_encode( [ 'posts' => $posts ] ); } wp_die(); } /** * Used to Add Sources * * * * @since 1.0 */ public static function add_source() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } $return = []; if (isset($_POST['provider']) && isset($_POST['providerIdUrl'])) { $provider = sanitize_text_field($_POST['provider']); $api_keys = get_option('sbr_apikeys', []); $has_api_key_in_post = isset($_POST['apiKey']) && !empty($_POST['apiKey']) && $_POST['apiKey'] !== null; $has_stored_api_key = isset($api_keys[$provider]) && !empty($api_keys[$provider]); // Process if API key is provided in POST OR if one is already stored for this provider if ($has_api_key_in_post || $has_stored_api_key) { $data = array( 'provider' => $provider, 'providerIdUrl' => esc_url_raw($_POST['providerIdUrl']), 'apiKey' => $has_api_key_in_post ? sanitize_text_field($_POST['apiKey']) : null, ); $return = self::process_source_apikey($data); } $return['freeRetrieverData'] = Util::get_free_retriever_data(); echo sbr_json_encode( $return ); } wp_die(); } /** * Used to Add Facebook Sources * * * * @since 1.0 */ public static function add_facebook_souce() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (isset($_POST['selectedFacebookPages']) && !empty($_POST['selectedFacebookPages'])) { $selected_facebook_pages = json_decode(stripslashes($_POST['selectedFacebookPages']), true); $encryption = new Data_Encryption(); $reporter = new Error_Reporter(); $reporter->remove_error('platform_data_deleted'); $reporter->reset_api_errors(); foreach ($selected_facebook_pages as $page) { $page['access_token'] = $encryption->maybe_encrypt($page['access_token']); SBR_Sources::update_or_insert($page); } } echo sbr_json_encode( [ 'sourcesCount' => SBR_Sources::get_sources_count(), 'sourcesList' => SBR_Sources::get_sources_list() ] ); wp_die(); } /** * Used to Add Manual Facebook Source * * * * @since 1.0 */ public static function add_manual_facebook_souce() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if ( isset($_POST['pageType']) && ! empty($_POST['pageType']) && isset($_POST['pageId']) && ! empty($_POST['pageId']) && isset($_POST['pageToken']) && ! empty($_POST['pageToken']) ) { $encryption = new Data_Encryption(); $access_token = $_POST['pageToken']; \SmashBalloon\Reviews\Pro\Integrations\Providers\Facebook::connect_manual_source($_POST['pageId'], $access_token); } wp_die(); } /** * Used to Add Sources * * * * @since 1.0 */ public static function delete_souce() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (isset($_POST['sourceID'], $_POST['sourceAccountID'], $_POST['sourceProvider'])) { $source_id = sanitize_text_field($_POST['sourceID']); $source_account_id = sanitize_text_field($_POST['sourceAccountID']); $provider = sanitize_text_field($_POST['sourceProvider']); // Get source info before deletion to extract relay_source_id if available $source_record = SBR_Sources::get_single_source_info([ 'id' => $source_account_id, 'provider' => $provider ]); $source_info = !empty($source_record['info']) ? json_decode($source_record['info'], true) : []; // For collections, just delete locally if (isset($_POST['isCollection']) && $_POST['isCollection']) { SBR_Sources::delete_source($source_id); PostAggregator::delete_reviews_by_provide_id($source_account_id); } else { // For relay-synced sources: delete from relay FIRST, then locally // This prevents orphaned sources if relay deletion fails $relay = new SBRelay(new SettingsManagerService()); // Use source_id if available (preferred - eliminates URL encoding issues) // Otherwise fall back to place_id for backward compatibility if (!empty($source_info['relay_source_id'])) { $relay_args = [ 'source_id' => (int) $source_info['relay_source_id'] ]; } else { $relay_args = [ 'place_id' => $source_account_id ]; } $relay_response = null; switch ($provider) { case 'google': $google = new Google($relay); $relay_response = $google->removeSource($relay_args); break; case 'yelp': $yelp = new Yelp($relay); $relay_response = $yelp->removeSource($relay_args); break; case 'tripadvisor': $tripadvisor = new \SmashBalloon\Reviews\Pro\Integrations\Providers\TripAdvisor($relay); $relay_response = $tripadvisor->removeSource($relay_args); break; case 'trustpilot': $trustpilot = new \SmashBalloon\Reviews\Pro\Integrations\Providers\TrustPilot($relay); $relay_response = $trustpilot->removeSource($relay_args); break; case 'wordpress.org': $WordpressOrg = new \SmashBalloon\Reviews\Pro\Integrations\Providers\WordpressOrg($relay); $relay_response = $WordpressOrg->removeSource($relay_args); break; } // Check relay response - only delete locally if relay succeeded or source doesn't exist // Allow deletion if: success, source not found (already deleted), or no relay_source_id (local-only) $relay_success = true; if (is_array($relay_response)) { // Check for error responses - but "not found" is OK (source already deleted from relay) if (isset($relay_response['error']) && !empty($relay_response['error'])) { $error_msg = is_string($relay_response['error']) ? $relay_response['error'] : ''; $is_not_found = stripos($error_msg, 'not found') !== false || stripos($error_msg, 'does not exist') !== false || (isset($relay_response['code']) && $relay_response['code'] === 404); if (!$is_not_found) { $relay_success = false; } } } if (!$relay_success) { // Relay deletion failed - don't delete locally to prevent orphaned state // Note: wp_send_json_error() terminates execution, no return needed wp_send_json_error([ 'message' => __('Failed to delete source from server. Please try again.', 'reviews-feed'), 'relay_error' => isset($relay_response['error']) ? $relay_response['error'] : 'Unknown error' ]); } // Relay deletion succeeded (or source wasn't on relay) - safe to delete locally SBR_Sources::delete_source($source_id); } } echo sbr_json_encode([ 'sourcesCount' => SBR_Sources::get_sources_count(), 'sourcesList' => SBR_Sources::get_sources_list(), 'freeRetrieverData' => Util::get_free_retriever_data() ]); wp_die(); } /** * Get impact data for deleting a source * Returns the count and names of feeds using this source * * @since 1.0 */ public static function get_source_impact() { check_ajax_referer('sbr-admin', 'nonce'); if (! sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (! isset($_POST['sourceAccountID'])) { wp_send_json_error([ 'message' => __('Source account ID is required', 'reviews-feed') ]); } $source_account_id = sanitize_text_field($_POST['sourceAccountID']); global $wpdb; $feeds_table = $wpdb->prefix . SBR_FEEDS_TABLE; // Get all feeds that use this source (by account_id in settings JSON) $feeds = $wpdb->get_results( $wpdb->prepare( "SELECT id, feed_name FROM $feeds_table WHERE settings LIKE %s ORDER BY feed_name ASC", '%' . $wpdb->esc_like($source_account_id) . '%' ), ARRAY_A ); $feeds_count = count($feeds); $max_display = 5; $has_more = $feeds_count > $max_display; // Only return first 5 feed names $feeds_to_return = array_slice($feeds, 0, $max_display); $feeds_list = array_map(function ($feed) { return [ 'id' => $feed['id'], 'name' => $feed['feed_name'], ]; }, $feeds_to_return); wp_send_json_success([ 'feeds_count' => $feeds_count, 'feeds' => $feeds_list, 'has_more' => $has_more, ]); } /** * import feed Settings * * * @since 1.0 */ public static function import_feed_settings() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } $filename = $_FILES['feedFile']['name']; $ext = pathinfo($filename, PATHINFO_EXTENSION); if ('json' !== $ext) { wp_send_json_error(['message' => __('JSON file needed. Your file is not in the correct format.', 'reviews-feed')]); } $imported_settings = file_get_contents($_FILES['feedFile']['tmp_name']); if (empty($imported_settings)) { wp_send_json_error(["message" => __("Don't have file, Please try again", "reviews-feed")]); } $feed_return = SBR_Feed_Saver_Manager::import_feed($imported_settings); // Note: wp_send_json() terminates execution, no wp_die() needed wp_send_json($feed_return, 200); } public static function check_api_limit($provider) { if (in_array($provider, ['trustpilot', 'wordpress.org'])) { return false; } $apikeys_limit = get_option('sbr_apikeys_limit', []); return is_array($apikeys_limit) && in_array($provider, $apikeys_limit); } public static function check_provider_apikey($provider) { $apikeys = get_option('sbr_apikeys', []); return is_array($apikeys) && isset($apikeys[$provider]) && !empty($apikeys[$provider]); } public static function limit_provider_api_calls($provider, $provider_id) { $retriever = Util::sbr_is_pro() ? new \SmashBalloon\Reviews\Pro\Utils\FreeRetriever() : new \SmashBalloon\Reviews\Common\Utils\FreeRetriever(); return !$retriever->check_api_call($provider, $provider_id); } public static function update_api_limit($provider, $action) { $apikeys_limit = get_option('sbr_apikeys_limit', []); if ($action === 'add') { if (!in_array($provider, $apikeys_limit, true)) { array_push($apikeys_limit, $provider); } } if ($action === 'delete') { if (in_array($provider, $apikeys_limit)) { $apikeys_limit = array_diff($apikeys_limit, [$provider]); } } update_option('sbr_apikeys_limit', $apikeys_limit); } /** * Add Or Update API Key * for a value * * @param string $provider * @param string $apikey * * * @since 1.0 */ public static function process_source_apikey($data) { $key_name = sanitize_key($data['provider']) . '_api_key'; $provider = isset($data['provider']) ? $data['provider'] : false; $api_keys = get_option('sbr_apikeys', []); $relay_args = []; $return = []; if ($provider !== false) { $providers_bulk = sbr_get_bulk_providers(); //If there is no providerIdUrl set, then the call is just for checking the API KEY switch ($provider) { case 'tripadvisor': $relay_args['place_id'] = isset($data['providerIdUrl']) ? self::get_place_id_tripadvisor($data['providerIdUrl']) : 'XXX'; break; case 'trustpilot': $relay_args['place_id'] = $data['providerIdUrl']; break; case 'wordpress.org': $wordpressorg_args = self::get_place_id_wordpressorg($data['providerIdUrl']); $relay_args['place_id'] = $wordpressorg_args['type'] . '/' . $wordpressorg_args['slug']; $relay_args['type'] = $wordpressorg_args['type']; $relay_args['slug'] = $wordpressorg_args['slug']; break; case 'woocommerce': case 'airbnb': case 'booking': case 'aliexpress': // These providers use dedicated AJAX endpoints // Return early with instruction to use specific endpoint return [ 'useSpecificEndpoint' => true, 'provider' => $provider, 'message' => 'Use provider-specific AJAX endpoint' ]; default: $relay_args['place_id'] = isset($data['providerIdUrl']) ? self::get_place_id($data['providerIdUrl']) : 'XXX'; $relay_args['check_api_key'] = true; break; } //Check if we need to add the API Key if (isset($data['apiKey']) && !empty($data['apiKey']) && !is_null($data['apiKey']) && $data['apiKey'] !== 'null') { $relay_args['api_key'] = $data['apiKey']; } elseif (isset($api_keys[$provider]) && !empty($api_keys[$provider]) && $api_keys[$provider] !== null) { $relay_args['api_key'] = $api_keys[$provider]; } if (!isset($relay_args['api_key']) || empty($relay_args['api_key']) || $relay_args['api_key'] === null || $relay_args['api_key'] === 'null') { unset($relay_args['api_key']); } //This Means that the users tries to get a new source //else tries to add API Key to be checked if (SBR_Feed_Saver_Manager::check_api_limit($provider) && !empty($relay_args['place_id']) && (empty($relay_args['api_key']) || is_null($relay_args['api_key']))) { return [ 'apiKeyLimits' => get_option('sbr_apikeys_limit', []), 'pluginNotices' => Util::get_plugin_notices(), 'error' => 'sourceLimitExceeded' ]; } $relay = new SBRelay(new SettingsManagerService()); $info = false; switch ($provider) { case 'google': $google = new Google($relay); $settings = wp_parse_args(get_option('sbr_settings', []), sbr_plugin_settings_defaults()); $relay_args['language'] = $settings['localization']; $info = $google->getSourcesInfo($relay_args); break; case 'yelp': $yelp = new Yelp($relay); $info = $yelp->getSourcesInfo($relay_args); break; case 'tripadvisor': $tripadvisor = new \SmashBalloon\Reviews\Pro\Integrations\Providers\TripAdvisor($relay); $info = $tripadvisor->getSourcesInfo($relay_args); break; case 'trustpilot': $trustpilot = new \SmashBalloon\Reviews\Pro\Integrations\Providers\TrustPilot($relay); $info = $trustpilot->getSourcesInfo($relay_args); break; case 'wordpress.org': $wordpressorg = new \SmashBalloon\Reviews\Pro\Integrations\Providers\WordpressOrg($relay); $info = $wordpressorg->getSourcesInfo($relay_args); break; } if ($info !== false) { $info = json_decode($info, true); //Checks if there is an error if ( !empty($info['error']) || (isset($info['success']) && false === $info['success']) ) { if ( !empty($info['error']) && $info['error'] === 'sourceLimitExceeded' ) { SBR_Feed_Saver_Manager::update_api_limit($provider, 'add'); } $info['apiKeyLimits'] = get_option('sbr_apikeys_limit', []); $info['pluginNotices'] = Util::get_plugin_notices(); return $info; } /** * This check for valid API key * When valid the API response may return the source info * OR * Return an error which if it's invalid location */ $checkValidKey = ( !empty($info['info']['error']) && $info['info']['error'] !== 'invalidKey' ) || !empty($info['info']['id']) || !empty($info['info']['successId']); if (isset($data['apiKey']) && $data['apiKey'] !== null && $data['apiKey'] !== 'null' && $checkValidKey) { self::update_provider_apikey($provider, $data['apiKey']); self::check_google_api_type($provider, $info); $return['apikey'] = 'valid'; SBR_Feed_Saver_Manager::update_api_limit($provider, 'delete'); $info['apiKeyLimits'] = get_option('sbr_apikeys_limit', []); } if (isset($info['info']['error'])) { if ($info['info']['error'] === 'invalidKey') { $return['apikey'] = 'invalid'; } if ($info['info']['error'] === 'invalidLocation') { $return['placeId'] = 'invalid'; } } if (isset($info['info']['id'])) { $info['info']['provider'] = $provider; // Decode HTML entities in name and id/URL (fixes Danish characters like æ, ø, å) $info['info']['name'] = htmlspecialchars_decode(html_entity_decode($info['info']['name'], ENT_QUOTES | ENT_HTML5, 'UTF-8'), ENT_QUOTES); $info['info']['id'] = html_entity_decode($info['info']['id'], ENT_QUOTES | ENT_HTML5, 'UTF-8'); if (isset($info['info']['url'])) { $info['info']['url'] = html_entity_decode($info['info']['url'], ENT_QUOTES | ENT_HTML5, 'UTF-8'); } // Capture relay_source_id for future API calls (eliminates URL encoding issues) if (isset($info['source_id'])) { $info['info']['relay_source_id'] = $info['source_id']; } if (isset($info['info']['reviews'])) { $reviews_list = $info['info']['reviews']; unset($info['info']['reviews']); self::cache_single_posts_from_set($reviews_list, $info['info']); } SBR_Sources::update_or_insert($info['info']); $return['message'] = 'addedSource'; $return['newAddedSource'] = $info['info']; SBR_Feed_Saver_Manager::update_api_limit($provider, 'delete'); if ( Util::sbr_is_pro() && in_array($provider, $providers_bulk) && !empty($data['apiKey']) && $data['apiKey'] !== "null" ) { $bulk_reviews = new \SmashBalloon\Reviews\Pro\Services\BulkUpdate\Bulk_Reviews_Update(); $bulk_reviews->schedule_task([ 'source_account_id' => $info['info']['id'], 'source_provider' => $provider ]); if ($bulk_reviews->check_account_option($info['info']['id'])) { $return['bulkStarted'] = true; } } } $return['apiKeys'] = get_option('sbr_apikeys', []); $return['sourcesList'] = SBR_Sources::get_sources_list(); } } $return['apiKeyLimits'] = get_option('sbr_apikeys_limit', []); $return['pluginNotices'] = Util::get_plugin_notices(); $return['sourcesCount'] = SBR_Sources::get_sources_count(); return $return; } public static function update_api_key() { if (! sbr_current_user_can('manage_reviews_feed_options') || ! check_ajax_referer('sbr-admin', 'nonce', false)) { wp_send_json_error([ "error" => "Unauthorized Access", "message" => "You are not allowed to perform this action." ]); } $return = []; if (isset($_POST['provider']) && isset($_POST['apiKey'])) { $provider = sanitize_text_field($_POST['provider']); $apiKey = sanitize_text_field($_POST['apiKey']); // External providers (airbnb, booking, aliexpress) don't require API keys // Keys are stored securely on the relay server $external_providers = ['airbnb', 'booking', 'aliexpress']; if (in_array($provider, $external_providers)) { $return = [ 'apiKeys' => get_option('sbr_apikeys', []), 'apikey' => 'not_required' ]; } elseif (!empty($_POST['removeApiKey']) && $_POST['removeApiKey'] === "true") { // Delete API Key from Plugin self::update_provider_apikey($provider, null); $return = [ 'apiKeys' => get_option('sbr_apikeys', []), 'apikey' => 'deleted' ]; } else { // Use existing logic for other providers $data = array( 'provider' => $provider, 'apiKey' => $apiKey ); $return = self::process_source_apikey($data); } } $return['freeRetrieverData'] = Util::get_free_retriever_data(); echo sbr_json_encode( $return ); wp_die(); } /** * Summary of check_google_api_type * * @param mixed $provider * @param mixed $info * * @return void */ public static function check_google_api_type($provider, $info) { if ( $provider === 'google' && !empty($info['info']['api_type']) ) { self::update_provider_apikey('googleApiType', $info['info']['api_type']); } } /** * Add Or Update API Key * for a value * * @param string $provider * @param string $apikey * * * @since 1.0 */ public static function update_provider_apikey($provider, $apikey) { $api_keys = get_option('sbr_apikeys', []); if ($apikey !== null && $apikey !== 'null') { $api_keys = !is_array($api_keys) ? [] : $api_keys; $api_keys[$provider] = $apikey; } if (empty($apikey) && isset($api_keys[$provider])) { unset($api_keys[$provider]); if ($provider === 'google') { unset($api_keys['googleApiType']); } } update_option('sbr_apikeys', $api_keys); } /** * Determines what table and sanitization should be used * when handling feed setting data. * * TODO: Add settings that need something other than sanitize_text_field * * @param string $key * * @return array * * @since 1.0 */ public static function get_data_type($key) { switch ($key) { case 'sources': $return = array( 'table' => 'feed_settings', 'sanitization' => 'sanitize_text_field', ); break; case 'feed_title': $return = array( 'table' => 'feeds', 'sanitization' => 'sanitize_text_field', ); break; case 'feed_name': $return = array( 'table' => 'feeds', 'sanitization' => 'sanitize_text_field', ); break; case 'status': $return = array( 'table' => 'feeds', 'sanitization' => 'sanitize_text_field', ); break; case 'author': $return = array( 'table' => 'feeds', 'sanitization' => 'int', ); break; default: $return = array( 'table' => 'feed_settings', 'sanitization' => 'sanitize_text_field', ); break; } return $return; } /** * Check if boolean * for a value * * @param int|string $value * * @return boolean * * @since 1.0 */ public static function is_boolean($value) { return ($value === 'true' || $value === 'false' || is_bool($value)) ? true : false; } /** * Get Place or Page ID from URL * * @param string $place_url * * @return string * * @since 1.0 */ public static function get_place_id($place_url) { $res = explode('/', $place_url); return end($res); } public static function get_place_id_tripadvisor($place_url) { // probably a place ID if ((bool) filter_var($place_url, FILTER_VALIDATE_URL) === false) { return $place_url; } $broken_up = explode('/', $place_url); foreach ($broken_up as $url_piece) { if (strpos($url_piece, '.html') !== false) { preg_match('/-d+\d{0,10}-/i', $url_piece, $matches, PREG_OFFSET_CAPTURE); if (! empty($matches[0])) { return str_replace(array('-', 'd'), '', $matches[0][0]); } } } return $place_url; } //WordPress Org Theme/Plugin Source public static function get_place_id_wordpressorg($place_url) { $broken_up = explode( '/', parse_url( trim($place_url, '/'), PHP_URL_PATH ) ); $slug = $broken_up[count($broken_up) - 1]; $type = strpos($place_url, 'theme') !== false ? 'theme' : 'plugin'; return [ 'type' => $type, 'slug' => $slug ]; } public static function cast_boolean($value) { if ($value === 'true' || $value === true || $value === 'on') { return true; } return false; } /** * Uses the appropriate sanitization function and returns the result * for a value * * @param string $type * @param int|string $value * * @return int|string * * @since 1.0 */ public static function sanitize($type, $value) { switch ($type) { case 'int': $return = intval($value); break; case 'boolean': $return = self::cast_boolean($value); break; default: $return = sanitize_text_field($value); break; } return $return; } public static function import_feed($json) { $feed_json = json_decode($json, true); $return = ['success' => false]; if (empty($feed_json['sourcesList'])) { $return['message'] = __('No feed source is included. Cannot upload feed.', 'reviews-feed'); return $return; } $settings_data = $feed_json['settings']; $feed_saver = new SBR_Feed_Saver(false); $feed_saver->set_data($settings_data); if ($feed_saver->update_or_insert()) { $return = array( 'success' => true, 'message' => __('Feed settings imported successfully.', 'reviews-feed'), 'feed_id' => $feed_saver->get_feed_id(), ); return $return; } else { $return['message'] = __('Could not import feed. Please try again', 'reviews-feed'); } return $return; } /** * Clear All Feeds Caches * * @return int|string * * @since 1.0 */ public static function clear_all_caches() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } self::clear_plugin_cache(); // Reset bulk update states to allow re-fetching of new reviews self::reset_bulk_update_states(); // Trigger an immediate cache refresh so users don't have to wait for // either the hourly cron or the next frontend visit. Two steps: // 1. Backdate `last_updated` on the rows we just emptied so the cron // query (`last_updated < now-12h` in FeedCacheUpdateService) picks // them up — clear_plugin_cache only nulls cache_value. // 2. Enqueue an immediate single event + spawn_cron loopback so the // refresh runs within ~1-2s, non-blocking for this AJAX response. // Gated by a filter so support can disable via mu-plugin if a customer // hits a runaway-fetch / quota incident without needing a redeploy. if (apply_filters('sbr_enable_immediate_refresh_on_clear', true)) { self::trigger_immediate_refresh(); } echo wp_json_encode([ 'success' => true ]); wp_die(); } /** * Trigger an immediate cron-driven refresh of just-cleared feed caches. * * Scoped to the admin AJAX path (NOT inside clear_plugin_cache) so the * 4 bulk-update finalizers that also call clear_plugin_cache stay * unchanged — they immediately rewrite their own rows and would only * waste a cron tick if backdated here. * * @since 2.5.6 */ private static function trigger_immediate_refresh(): void { global $wpdb; // Backdate `last_updated` so the cron query picks the cleared rows up. // This is idempotent — repeated clicks just re-write the same epoch // sentinel. Safe to do even when the cron-trigger guard below skips // the schedule + spawn_cron — the hourly cron will catch up. $cache_table = $wpdb->prefix . 'sbr_feed_caches'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->query( "UPDATE $cache_table SET last_updated = '1970-01-01 00:00:00' WHERE cache_key NOT IN ('posts_backup', 'header_backup')" ); // Application-level dedup: if a refresh was already triggered within // the last 60s, skip scheduling a second cron + spawn_cron loopback. // WP's wp_schedule_single_event dedup window is 10 minutes, but the // loopback removes the event from the queue as soon as cron PHP starts // (before upstream completes), so a 2nd click ~1s later could schedule // a fresh event and double-fetch. This transient locks at our layer. $trigger_guard = 'sbreviews_immediate_refresh_in_progress'; if (false !== get_transient($trigger_guard)) { return; } set_transient($trigger_guard, 1, 60); if (function_exists('wp_schedule_single_event')) { wp_schedule_single_event(time(), \SmashBalloon\Reviews\Common\Services\FeedCacheUpdateService::CRON_JOB_NAME); } if (function_exists('spawn_cron')) { spawn_cron(); } } /** * Reset bulk update states for all providers * * This triggers a full resync of reviews from all sources: * - Google + Yelp: Drop the per-source bulk-history option so the * "Unable to retrieve reviews history" warning clears, and reschedule * bulk-history for any source whose provider has an API key configured * - External providers (Airbnb, Booking, AliExpress): Re-fetch from relay API * - WooCommerce: Resync from wp_comments table * * WooCommerce is included to provide users a "nuclear option" for full refresh. * For ongoing changes, event-driven hooks handle individual review updates. * * @see documentation/WOOCOMMERCE_EVENT_DRIVEN_CACHE_ARCHITECTURE.md * * @since 2.3.0 */ private static function reset_bulk_update_states(): void { // Google + Yelp: drop the per-source bulk-history state so the source // list warning clears, then reschedule bulk-history for any provider // with an API key configured. Same gating as BulkHistoryRoutine — keyless // customers stay keyless, but they no longer see a permanent warning // rooted in a stale option (regression-pin: pre-fix Bulk_Reviews_Update // could leave entries at {retry: true, is_done: false} indefinitely with // no UI path to clear them). if (Util::sbr_is_pro() && class_exists(Bulk_Reviews_Update::class)) { delete_option('sbr_bulk_sources'); // Clear any in-flight bulk-cron events before rescheduling so a // repeated "Clear All Caches" click doesn't enqueue a backlog. // schedule_task uses wp_schedule_single_event which dedupes within // 10 minutes for identical args, but Clear All Caches is on a manual // click cadence — defensive cleanup is the safer contract. if (function_exists('wp_clear_scheduled_hook')) { wp_clear_scheduled_hook('sbr_reviews_bulk_cron'); } Bulk_Reviews_Update::schedule_needed_sources_history(); } // External providers (Airbnb, Booking, AliExpress): Reset and schedule background fetch if (Util::sbr_is_pro() && class_exists(Bulk_External_Reviews_Update::class)) { Bulk_External_Reviews_Update::reset_all_sources(true); } // WooCommerce: Resync all reviews from wp_comments table // This provides users a way to force a full refresh if needed if (Util::sbr_is_pro() && class_exists(Bulk_WooCommerce_Reviews_Update::class)) { Bulk_WooCommerce_Reviews_Update::reset_all_sources(true); } } /** * Clear All Plugin Caches * * @return void * * @since 1.0 */ public static function clear_plugin_cache() { global $wpdb; $cache_table_name = $wpdb->prefix . 'sbr_feed_caches'; $sql = " UPDATE $cache_table_name SET cache_value = '' WHERE cache_key NOT IN ('posts_backup', 'header_backup');"; $wpdb->query($sql); $posts_table_name = $wpdb->prefix . 'sbr_reviews_posts'; $sql_posts = " UPDATE $posts_table_name SET images_done = 0"; $wpdb->query($sql_posts); $table_name = $wpdb->prefix . "options"; $wpdb->query(" DELETE FROM $table_name WHERE `option_name` LIKE ('%\_transient\_sbr\_%') "); $wpdb->query(" DELETE FROM $table_name WHERE `option_name` LIKE ('%\_transient\_sbr\_ej\_%') "); $wpdb->query(" DELETE FROM $table_name WHERE `option_name` LIKE ('%\_transient\_sbr\_tle\_%') "); $wpdb->query(" DELETE FROM $table_name WHERE `option_name` LIKE ('%\_transient\_sbr\_album\_%') "); $wpdb->query(" DELETE FROM $table_name WHERE `option_name` LIKE ('%\_transient\_timeout\_sbr\_%') "); self::flush_third_party_caches(); } /** * Purge known 3rd-party page-cache plugins (WP Rocket, W3TC, LiteSpeed, etc). * * Called twice per Clear-All-Caches cycle: * 1. Inside clear_plugin_cache() — clears stale HTML right after the feed * cache is emptied. * 2. After the async cron refresh completes (via `sbr_after_cron_refresh` * handler in register()) — re-flushes so page caches that rebaked the * empty-feed render during the 5-30s warm-up window are evicted and * the next visitor gets a fresh render off the now-warm feed cache. * * @since 2.5.6 */ public static function flush_third_party_caches(): void { // Clear cache of major caching plugins. if (isset($GLOBALS['wp_fastest_cache']) && method_exists($GLOBALS['wp_fastest_cache'], 'deleteCache')) { $GLOBALS['wp_fastest_cache']->deleteCache(); } // WP Super Cache. if (function_exists('wp_cache_clear_cache')) { wp_cache_clear_cache(); } // W3 Total Cache. if (function_exists('w3tc_flush_all')) { w3tc_flush_all(); } if (function_exists('sg_cachepress_purge_cache')) { sg_cachepress_purge_cache(); } if (class_exists('W3_Plugin_TotalCacheAdmin')) { $plugin_totalcacheadmin = & w3_instance('W3_Plugin_TotalCacheAdmin'); $plugin_totalcacheadmin->flush_all(); } if (function_exists('rocket_clean_domain')) { rocket_clean_domain(); } if (has_action('litespeed_purge_all')) { do_action('litespeed_purge_all'); } } public static function sanitize_settings($raw_settings) { $sanitized_values = array(); foreach ($raw_settings as $key => $value) { if (is_bool($value)) { // already a safe value $sanitized_values[$key] = $value; } elseif (is_int($value)) { // already a safe value $sanitized_values[$key] = $value; } elseif (is_array($value)) { if (empty($value)) { $sanitized_values[$key] = array(); } else { foreach ($value as $key2 => $item) { if (is_bool($item)) { // already a safe value $sanitized_values[$key][$key2] = $item; } elseif (is_int($item)) { // already a safe value $sanitized_values[$key][$key2] = $item; } else { $sanitized_values[$key][$key2] = sanitize_text_field($item); } } } } else { $sanitized_values[$key] = sanitize_text_field($value); } } return $sanitized_values; } /** * Get a list of feeds with a limit and offset like a page * * @since 1.1 */ public static function get_feed_list_page() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } $args = array( 'page' => isset($_POST['page']) ? (int) sanitize_text_field(wp_unslash($_POST['page'])) : 1 ); // Add search parameter if provided if (!empty($_POST['search'])) { $args['search'] = sanitize_text_field(wp_unslash($_POST['search'])); } $feeds_data = DB::get_feeds_list($args); $total_count = DB::feeds_list_count($args); $per_page = DB::RESULTS_PER_PAGE; echo wp_json_encode([ 'feeds' => $feeds_data, 'total' => $total_count, 'page' => $args['page'], 'perPage' => $per_page, 'totalPages' => ceil($total_count / $per_page) ]); wp_die(); } /** * Cache Single Posts form Posts List * * @since 1..1 */ public static function cache_single_posts_from_set($posts, $provider) { $settings = wp_parse_args(get_option('sbr_settings', []), sbr_plugin_settings_defaults()); $lang = $settings['localization']; $providers_no_media = sbr_get_no_media_providers(); $providers_lang = sbr_get_lang_providers(); foreach ($posts as $single_review) { self::cache_single_review( $single_review, $provider, $lang, $providers_no_media, $providers_lang ); } } public static function cache_single_review( $single_review, $provider, $lang, $providers_no_media, $providers_lang ) { // Decode HTML entities in review text and reviewer name (fixes Danish characters, emojis, etc.) if (isset($single_review['text'])) { $single_review['text'] = html_entity_decode($single_review['text'], ENT_QUOTES | ENT_HTML5, 'UTF-8'); } if (isset($single_review['reviewer']['name'])) { $single_review['reviewer']['name'] = html_entity_decode($single_review['reviewer']['name'], ENT_QUOTES | ENT_HTML5, 'UTF-8'); } $single_review['source'] = $provider; $single_post_cache = Util::sbr_is_pro() ? new \SmashBalloon\Reviews\Pro\SinglePostCache($single_review, new \SmashBalloon\Reviews\Pro\MediaFinder($single_review['source'])) : new \SmashBalloon\Reviews\Common\SinglePostCache($single_review); $single_post_cache->set_provider_id($provider['id']); if (in_array($provider['provider'], $providers_lang, true)) { $single_post_cache->set_lang($lang); } if ( Util::sbr_is_pro() && method_exists($single_post_cache, 'check_api_media') ) { $single_post_cache->check_api_media(); } if (! $single_post_cache->db_record_exists()) { $single_post_cache->resize_avatar(150); if (in_array($provider['provider'], $providers_no_media, true)) { $single_post_cache->set_storage_data('images_done', 1); } $single_post_cache->store(); } else { $single_post_cache->update_single(); } } /** * Create Or Update Collection Review * * @since 1.3 */ public static function create_new_collection() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (isset($_POST['collection_name']) && !empty($_POST['collection_name'])) { $id = 'collection-' . time() . wp_rand(); $collection_name = sanitize_text_field($_POST['collection_name']); $collection_info = [ 'id' => $id, 'account_id' => $id, 'name' => $collection_name, 'provider' => 'collection', 'rating' => 0, 'total_rating' => 0, ]; SBR_Sources::update_or_insert($collection_info); $collection_info['info'] = $collection_info; echo sbr_json_encode( [ 'newCollection' => $collection_info, 'sourcesCount' => SBR_Sources::get_sources_count(), 'sourcesList' => SBR_Sources::get_sources_list() ] ); } wp_die(); } /** * Create Or Update Collection Review * * @since 1.3 */ public static function create_update_collection_review() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } $review = json_decode(stripslashes($_POST['reviewContent']), true); if (!isset($review['provider_id']) || empty($review['provider_id'])) { wp_send_json_error( [ "message" => __("Please make sure the collection exists", "reviews-feed") ] ); } $is_duplicate = isset($_POST['is_duplicate']) && $_POST['is_duplicate']; $is_new = (isset($review['new']) && $review['new'] === true) || $is_duplicate; $provider_id = sanitize_text_field($review['provider_id']); $page_number = absint($_POST['page_number']); $review_id = $is_new ? $provider_id . time() . wp_rand() : sanitize_text_field($review['review_id']); $time = isset($review['time']) ? sanitize_text_field($review['time']) : ''; $sanitized_review = [ 'time' => empty($time) ? time() : (($is_new && !$is_duplicate) || !is_numeric($time) ? strtotime($time) : $time), 'rating' => absint($review['rating']), 'provider_id' => $provider_id, 'review_id' => $review_id, 'text' => sanitize_text_field($review['text']), 'title' => sanitize_text_field($review['title']), 'reviewer' => [ 'name' => sanitize_text_field($review['reviewer']['first_name']) . ' ' . sanitize_text_field($review['reviewer']['last_name']), 'first_name' => sanitize_text_field($review['reviewer']['first_name']), 'last_name' => sanitize_text_field($review['reviewer']['last_name']), 'avatar' => sanitize_text_field($review['reviewer']['avatar']) ], 'provider' => [ 'name' => sanitize_text_field($review['provider']['name']) ], 'source' => [ 'id' => $provider_id, 'url' => sanitize_text_field($review['source']['url']) ] ]; if (isset($review['media']) && is_array($review['media'])) { $sanitized_review['media'] = $review['media']; } //JSON Data $sanitized_review['json_data'] = $sanitized_review; $single_post_cache = new \SmashBalloon\Reviews\Pro\SinglePostCache($sanitized_review, new \SmashBalloon\Reviews\Pro\MediaFinder($sanitized_review['source'])); $single_post_cache->set_provider_id($provider_id); if ($is_new) { $single_post_cache->store(); } else { $single_post_cache->update_single(); } if (isset($sanitized_review['media']) && Util::should_store_local_images()) { $aggregator = new PostAggregator(); $single_post_cache->resize_images(array(640, 150)); $single_post_data = $single_post_cache->get_post_data(); $single_post_storage_data = $single_post_cache->get_storage_data(); $single_post_data = $aggregator->add_local_image_urls($single_post_data, $single_post_storage_data); $single_post_cache->update( [ array('images_done', 1, '%d'), array('json_data', wp_json_encode($single_post_data), '%s') ] ); } SBR_Sources::update_collection_ratings($provider_id); echo sbr_json_encode( [ 'postsList' => PostAggregator::get_source_posts_list($provider_id, $page_number) ] ); wp_die(); } /** * Update Collection Name * * @since 1.3 */ public static function update_collection_name() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if ( isset($_POST['id'], $_POST['collection_name'], $_POST['provider_id'], $_POST['provider']) && !empty($_POST['collection_name']) && !empty($_POST['provider_id']) && !empty($_POST['provider']) && !empty($_POST['id']) ) { $id = sanitize_text_field($_POST['id']); $provider = sanitize_text_field($_POST['provider']); $provider_id = sanitize_text_field($_POST['provider_id']); $collection_name = sanitize_text_field($_POST['collection_name']); $collection_info_search = [ 'id' => $provider_id, 'provider' => $provider ]; $db = new DB(); $results = $db->get_single_source($collection_info_search); if (sizeof($results) === 0) { wp_send_json_error( [ "message" => __("Please make sure the collection exists", "reviews-feed") ] ); } $collection_db = $results[0]; $collection_db['name'] = $collection_name; /** @phpstan-ignore-next-line info key exists in database results */ unset($collection_db['info']); $collection_db['last_updated'] = date('Y-m-d H:i:s'); $collection_db['id'] = $provider_id; $collection_db['info'] = wp_json_encode($collection_db); SBR_Sources::update($collection_db); echo sbr_json_encode( [ 'newCollectionData' => $collection_db, 'sourcesCount' => SBR_Sources::get_sources_count(), 'sourcesList' => SBR_Sources::get_sources_list() ] ); } wp_die(); } /** * Update Source Provider Posts List * * @since 1.4 */ public static function get_source_posts() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if ( isset($_POST['provider_id'], $_POST['provider'], $_POST['page_number']) && !empty($_POST['provider_id']) && !empty($_POST['provider']) && !empty($_POST['page_number']) ) { $provider = sanitize_text_field($_POST['provider']); $provider_id = sanitize_text_field($_POST['provider_id']); $page_number = absint($_POST['page_number']); echo sbr_json_encode( [ 'postsList' => PostAggregator::get_source_posts_list($provider_id, $page_number) ] ); } wp_die(); } /** * Delete Review from a Collection * * @since 1.4 */ public static function delete_review_from_collection() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if ( isset($_POST['review_id'], $_POST['provider_id'], $_POST['provider'], $_POST['page_number']) && !empty($_POST['review_id']) && !empty($_POST['provider_id']) && !empty($_POST['provider']) && !empty($_POST['page_number']) ) { $provider = sanitize_text_field($_POST['provider']); $review_id = sanitize_text_field($_POST['review_id']); $provider_id = sanitize_text_field($_POST['provider_id']); $page_number = absint($_POST['page_number']); PostAggregator::delete_review($provider_id, $review_id, $provider); SBR_Sources::update_collection_ratings($provider_id); echo sbr_json_encode( [ 'postsList' => PostAggregator::get_source_posts_list($provider_id, $page_number) ] ); } wp_die(); } /** * Add Multiple Selected Reviews to a Collection * * @since 1.4 */ public static function add_multiple_reviews_collection() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if ( isset($_POST['provider_id'], $_POST['selected_reviews']) && !empty($_POST['provider_id']) && !empty($_POST['selected_reviews']) ) { $provider_id = sanitize_text_field($_POST['provider_id']); $selected_reviews = json_decode(stripslashes($_POST['selected_reviews'])); PostAggregator::insert_multiple_reviews($provider_id, $selected_reviews); echo sbr_json_encode( [ 'postsList' => PostAggregator::get_source_posts_list($provider_id, 1) ] ); wp_die(); } } /** * Advanced Reviews Search * * @since 1.4 */ public static function advanced_search_reviews() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if ( isset($_POST['provider_id'], $_POST['search_text']) && !empty($_POST['provider_id']) && !empty($_POST['search_text']) ) { $provider_id = sanitize_text_field($_POST['provider_id']); $search_text = sanitize_text_field($_POST['search_text']); echo sbr_json_encode( [ 'postsList' => PostAggregator::search_source_posts_list($provider_id, $search_text) ] ); wp_die(); } } /** * Duplicate Collection * * @since 1.4 */ public static function duplicate_collection() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (!empty($_POST['collection_id'])) { $collection_id = sanitize_text_field($_POST['collection_id']); $db = new DB(); $args = [ 'id' => $collection_id, 'provider' => 'collection' ]; $results = $db->get_single_source($args); if (!isset($results[0])) { wp_send_json_error( [ 'message' => __('Cannot duplicate Collection, something went wrong!', 'reviews-feed') ] ); } $collection = $results[0]; $collection_info = json_decode($collection['info'], true); $id = 'collection-' . time() . wp_rand(); $collection_insert = [ 'id' => $id, 'account_id' => $id, 'name' => $collection['name'] . ' (copy)', 'provider' => 'collection', 'rating' => $collection_info['rating'], 'total_rating' => $collection_info['total_rating'] ]; SBR_Sources::update_or_insert($collection_insert); $collection_reviews = PostAggregator::get_source_all_posts($collection_id); foreach ($collection_reviews as $s_review) { $review_json = json_decode($s_review['json_data'], true); $review_id = $id . time() . wp_rand(); $sanitized_review = [ 'time' => absint($review_json['time']), 'rating' => absint($review_json['rating']), 'provider_id' => $id, 'review_id' => $review_id, 'text' => sanitize_text_field($review_json['text']), 'title' => sanitize_text_field($review_json['title']), 'reviewer' => [ 'name' => sanitize_text_field($review_json['reviewer']['name']), 'first_name' => isset($review_json['reviewer']['first_name']) ? sanitize_text_field($review_json['reviewer']['first_name']) : '', 'last_name' => isset($review_json['reviewer']['last_name']) ? sanitize_text_field($review_json['reviewer']['last_name']) : '', 'avatar' => sanitize_text_field($review_json['reviewer']['avatar']) ], 'provider' => [ 'name' => sanitize_text_field($review_json['provider']['name']) ], 'source' => [ 'id' => $id, 'url' => sanitize_text_field($review_json['source']['url']) ] ]; if (isset($review['media']) && is_array($review_json['media'])) { $sanitized_review['media'] = $review_json['media']; } //JSON Data $sanitized_review['json_data'] = $sanitized_review; $single_post_cache = new \SmashBalloon\Reviews\Pro\SinglePostCache($sanitized_review, new \SmashBalloon\Reviews\Pro\MediaFinder($sanitized_review['source'])); $single_post_cache->set_provider_id($id); $single_post_cache->store(); /** @phpstan-ignore-next-line media key may or may not exist depending on review data */ if (isset($sanitized_review['media']) && Util::should_store_local_images()) { $aggregator = new PostAggregator(); $single_post_cache->resize_images(array(640, 150)); $single_post_data = $single_post_cache->get_post_data(); $single_post_storage_data = $single_post_cache->get_storage_data(); $single_post_data = $aggregator->add_local_image_urls($single_post_data, $single_post_storage_data); $single_post_cache->update( [ array('images_done', 1, '%d'), array('json_data', wp_json_encode($single_post_data), '%s') ] ); } } echo sbr_json_encode( [ 'sourcesCount' => SBR_Sources::get_sources_count(), 'sourcesList' => SBR_Sources::get_sources_list() ] ); wp_die(); } } /** * Load More Sources * * @since 1.4 */ public static function load_more_sources() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } $args = array( 'page' => isset($_POST['sources_page']) ? absint($_POST['sources_page']) : 1 ); // Add search parameter if provided if (!empty($_POST['search'])) { $args['search'] = sanitize_text_field(wp_unslash($_POST['search'])); } $sources_list = SBR_Sources::get_sources_list($args); $total_count = SBR_Sources::get_sources_count($args); $per_page = DB::SOURCES_PER_PAGE; echo sbr_json_encode([ 'sourcesList' => $sources_list, 'sourcesCount' => $total_count, 'page' => $args['page'], 'perPage' => $per_page, 'totalPages' => ceil($total_count / $per_page) ]); wp_die(); } /** * Export Single Collection Reviews * * @since 1.4 */ public static function export_collection() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (!empty($_POST['collection_id'])) { $collection_id = sanitize_text_field($_POST['collection_id']); $collection_data = DB::get_collections_reviews($collection_id); //Will Return Collection Data + The Reviews List echo sbr_json_encode( [ 'collection' => $collection_data ] ); } wp_die(); } /** * IMport Full Collection Data * * * @since 1.0 */ public static function import_full_collection() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } $collection_json = self::parse_collection_json(); $info = json_decode($collection_json['info'], true); if (!empty($info)) { $collection_info = [ 'id' => sanitize_text_field($collection_json['account_id']), 'account_id' => sanitize_text_field($collection_json['account_id']), 'name' => sanitize_text_field($collection_json['name']), 'provider' => 'collection', 'rating' => absint($info['rating']), 'total_rating' => absint($info['total_rating']), ]; SBR_Sources::update_or_insert($collection_info); foreach ($collection_json['reviewsList'] as $single_review) { unset($single_review['id']); $single_review = json_decode($single_review['json_data'], true); if (!empty($single_review)) { $review_store = Util::parse_single_review($single_review, $collection_json['account_id'], $single_review['review_id']); $review_store['json_data'] = $review_store; $single_post_cache = new \SmashBalloon\Reviews\Pro\SinglePostCache($review_store, new \SmashBalloon\Reviews\Pro\MediaFinder($review_store['source'])); $single_post_cache->set_provider_id($collection_json['account_id']); $single_post_cache->store(); } } } echo sbr_json_encode( [ 'message' => __("Collection imported successfully", "reviews-feed") ] ); wp_die(); } /** * Import Only reviews to Collection * * * @since 1.0 */ public static function import_reviews_collection() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (!empty($_POST['collection_id'])) { $collection_json = self::parse_collection_json(); $collection_id = sanitize_text_field($_POST['collection_id']); foreach ($collection_json['reviewsList'] as $single_review) { unset($single_review['id']); $single_review = json_decode($single_review['json_data'], true); if (!empty($single_review)) { $single_review['review_id'] = $collection_id . time() . wp_rand(); $single_review['retimeview_id'] = time(); $review_store = Util::parse_single_review($single_review, $collection_id, $single_review['review_id']); $review_store['json_data'] = $review_store; $single_post_cache = new \SmashBalloon\Reviews\Pro\SinglePostCache($review_store, new \SmashBalloon\Reviews\Pro\MediaFinder($review_store['source'])); $single_post_cache->set_provider_id($collection_id); $single_post_cache->store(); if (isset($sanitized_review['media']) && Util::should_store_local_images()) { $aggregator = new PostAggregator(); $single_post_cache->resize_images(array(640, 150)); $single_post_data = $single_post_cache->get_post_data(); $single_post_storage_data = $single_post_cache->get_storage_data(); $single_post_data = $aggregator->add_local_image_urls($single_post_data, $single_post_storage_data); $single_post_cache->update( [ array('images_done', 1, '%d'), array('json_data', wp_json_encode($single_post_data), '%s') ] ); } } } SBR_Sources::update_collection_ratings($collection_id); echo sbr_json_encode( [ 'postsList' => PostAggregator::get_source_posts_list($collection_id, 1) ] ); } wp_die(); } /** * Parse Collections JSON file * * * @since 1.0 */ public static function parse_collection_json() { check_ajax_referer('sbr-admin', 'nonce'); if (!sbr_current_user_can('manage_reviews_feed_options')) { wp_send_json_error(); } if (empty($_FILES['feedFile'])) { wp_send_json_error( [ 'message' => __('JSON file needed. Your file is not in the correct format.', 'reviews-feed') ] ); } $filename = $_FILES['feedFile']['name']; $ext = pathinfo($filename, PATHINFO_EXTENSION); if ('json' !== $ext) { wp_send_json_error( [ 'message' => __('JSON file needed. Your file is not in the correct format.', 'reviews-feed') ] ); } $imported_file = file_get_contents($_FILES['feedFile']['tmp_name']); if (empty($imported_file)) { wp_send_json_error( [ "message" => __("JSON file needed. Your file is not in the correct format.", "reviews-feed") ] ); } $collection_json = json_decode($imported_file, true); if (empty($collection_json['reviewsList'])) { wp_send_json_error( [ "message" => __("Something went wrong. This doesn't look like a collections import file.", "reviews-feed") ] ); } return $collection_json; } }