PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / 2.8.0
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More v2.8.0
2.11.0 2.10.0 2.9.0 2.8.0 2.7.0 2.6.7 2.6.8 2.6.5 2.6.4 2.6.3 2.6.2 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 trunk 1.0 1.0.1 1.0.2 1.0.3 1.1 1.1.1 1.1.2 1.2.0 2.0 2.1.0 2.1.1 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 2.4.5 2.4.6 2.5.0 2.5.1
reviews-feed / class / Common / Util.php
reviews-feed / class / Common Last commit date
Admin 1 month ago Builder 1 month ago Customizer 1 month ago Exceptions 1 month ago Helpers 1 month ago Integrations 1 month ago Migrations 1 month ago ReviewAlerts 1 month ago Services 1 month ago Settings 1 month ago Support 1 month ago Traits 1 month ago Utils 1 month ago AuthorizationStatusCheck.php 1 month ago BusinessDataCache.php 1 month ago Clear_Cache.php 1 month ago Container.php 1 month ago DisplayElements.php 1 month ago Email_Notification.php 1 month ago Error_Reporter.php 1 month ago Feed.php 1 month ago FeedCache.php 1 month ago FeedCacheUpdater.php 1 month ago FeedDisplay.php 1 month ago Feed_Locator.php 1 month ago Parser.php 1 month ago PostAggregator.php 1 month ago RemoteRequest.php 1 month ago SBR_Education.php 1 month ago SBR_Settings.php 1 month ago ServiceContainer.php 1 month ago SinglePostCache.php 1 month ago TemplateRenderer.php 1 month ago Tooltip_Wizard.php 1 month ago Util.php 1 month ago
Util.php
1702 lines
1 <?php
2
3 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
4 // Note: Table name interpolation is safe (uses internal constants, not user input).
5
6 namespace SmashBalloon\Reviews\Common;
7
8 use SmashBalloon\Reviews\Common\Builder\SBR_Sources;
9 use SmashBalloon\Reviews\Common\Customizer\DB;
10 use SmashBalloon\Reviews\Common\Helpers\SBR_Error_Handler;
11 use SmashBalloon\Reviews\Pro\Integrations\Providers\EDD;
12
13 class Util
14 {
15 public static function isProduction()
16 {
17 return SBR_PRODUCTION;
18 }
19
20 public static function UTMCampaign()
21 {
22 return '';
23 }
24
25 /**
26 * Get feature flags from config file
27 *
28 * @return array
29 */
30 public static function get_feature_flags()
31 {
32 $config_file = SBR_PLUGIN_DIR . 'config/feature-flags.php';
33 if (file_exists($config_file)) {
34 return include $config_file;
35 }
36 return [];
37 }
38
39 /**
40 * Check if a provider is disabled via feature flags
41 *
42 * @param string $provider_type Provider type to check
43 * @return bool
44 */
45 public static function is_provider_disabled($provider_type)
46 {
47 $flags = self::get_feature_flags();
48 $disabled = isset($flags['disabled_providers']) ? $flags['disabled_providers'] : [];
49 return in_array($provider_type, $disabled, true);
50 }
51
52 /**
53 * Resolve EDD's pluginRequired / pluginRequiredMessage for the customizer
54 * provider tile. Strict: in Pro the gate requires both EDD core AND the
55 * EDD Reviews extension — otherwise the modal would let users add a source
56 * the integration can't actually capture new reviews for.
57 *
58 * Free builds may not ship class/Pro/Integrations/Providers/EDD.php, so
59 * fall back to the prior core-only check there. Free never reaches this
60 * tile in the disabled-from-extension state anyway (EDD shows as an
61 * upsell, not a regular provider), but the fallback keeps parity with
62 * pre-existing behavior.
63 *
64 * @return array Two-key map: pluginRequired (bool), pluginRequiredMessage (string)
65 */
66 private static function get_edd_provider_status()
67 {
68 if (class_exists(EDD::class)) {
69 return [
70 'pluginRequired' => ! EDD::is_active_static(),
71 'pluginRequiredMessage' => EDD::plugin_required_message(),
72 ];
73 }
74
75 return [
76 'pluginRequired' => ! ( class_exists('Easy_Digital_Downloads') || defined('EDD_VERSION') ),
77 'pluginRequiredMessage' => __('Enable Easy Digital Downloads plugin to use it as a source', 'reviews-feed'),
78 ];
79 }
80
81 public static function get_providers()
82 {
83 $campaign = self::sbr_is_pro() ? 'reviews-pro' : 'reviews-free';
84
85 $providers = [
86 [
87 'type' => 'google',
88 'name' => 'Google',
89 'heading' => __('Place ID', 'reviews-feed'),
90 'placeholder' => __('Enter Place ID', 'reviews-feed'),
91 'docLink' => 'https://smashballoon.com/doc/creating-a-google-api-key/?utm_campaign=' . $campaign . '&utm_source=settings&utm_medium=docs'
92 ],
93 [
94 'type' => 'facebook',
95 'name' => 'Facebook'
96 ],
97 [
98 'type' => 'tripadvisor',
99 'name' => 'TripAdvisor',
100 'heading' => __('Page URL', 'reviews-feed'),
101 'placeholder' => __('https://tripadvisor.com/...', 'reviews-feed'),
102 // API key is OPTIONAL: the relay falls back to a shared TripAdvisor
103 // Content API key when none is sent, so the add-source flow must keep
104 // the Skip button. 'apiKey' => true still shows the (optional) key step;
105 // dropping 'mandatoryApiKey' stops the customizer from hiding Skip and
106 // gating Next on a non-empty field. (SMASH-1690 / WPSA #71949)
107 'apiKey' => true,
108 'docLink' => 'https://smashballoon.com/doc/creating-a-tripadvisor-api-key/?utm_campaign=' . $campaign . '&utm_source=settings&utm_medium=docs'
109 ],
110 [
111 'type' => 'yelp',
112 'name' => 'Yelp',
113 'heading' => __('Page URL', 'reviews-feed'),
114 'placeholder' => __('https://yelp.com/...', 'reviews-feed'),
115 'docLink' => 'https://smashballoon.com/doc/creating-a-yelp-api-key/?utm_campaign=' . $campaign . '&utm_source=settings&utm_medium=docs'
116 ],
117 [
118 'type' => 'trustpilot',
119 'name' => 'TrustPilot',
120 'heading' => __('Page URL', 'reviews-feed'),
121 'placeholder' => __('https://trustpilot.com/review/...', 'reviews-feed'),
122 'onlyDetails' => true
123 ],
124 [
125 'type' => 'wordpress.org',
126 'name' => 'WordPress.org',
127 'heading' => __('Page URL', 'reviews-feed'),
128 'placeholder' => __('https://wordpress.org/...', 'reviews-feed'),
129 'onlyDetails' => true
130 ],
131 [
132 'type' => 'woocommerce',
133 'name' => 'WooCommerce',
134 'heading' => __('Product', 'reviews-feed'),
135 'placeholder' => __('Select a product', 'reviews-feed'),
136 'onlyDetails' => true,
137 'isLocal' => true,
138 'pluginRequired' => ! ( class_exists('WooCommerce') || function_exists('WC') ),
139 'pluginRequiredMessage' => __('Enable WooCommerce plugin to use it as a source', 'reviews-feed'),
140 ],
141 array_merge(
142 [
143 'type' => 'edd',
144 'name' => 'Easy Digital Downloads',
145 'heading' => __('Download', 'reviews-feed'),
146 'placeholder' => __('Select a download', 'reviews-feed'),
147 'onlyDetails' => true,
148 'isLocal' => true,
149 ],
150 self::get_edd_provider_status()
151 ),
152 [
153 'type' => 'airbnb',
154 'name' => 'Airbnb',
155 'heading' => __('Listing URL', 'reviews-feed'),
156 'placeholder' => __('https://www.airbnb.com/rooms/...', 'reviews-feed'),
157 'onlyDetails' => true
158 ],
159 [
160 'type' => 'booking',
161 'name' => 'Booking.com',
162 'heading' => __('Hotel URL', 'reviews-feed'),
163 'placeholder' => __('https://www.booking.com/hotel/...', 'reviews-feed'),
164 'onlyDetails' => true
165 ],
166 [
167 'type' => 'aliexpress',
168 'name' => 'AliExpress',
169 'heading' => __('Product URL', 'reviews-feed'),
170 'placeholder' => __('https://www.aliexpress.com/item/...', 'reviews-feed'),
171 'onlyDetails' => true
172 ]
173 ];
174
175 // Filter out disabled providers
176 $providers = array_filter($providers, function ($provider) {
177 return ! self::is_provider_disabled($provider['type']);
178 });
179
180 return array_values($providers);
181 }
182
183 /**
184 * Used as a listener for the account connection process. If
185 * data is returned from the account connection processed it's used
186 * to generate the list of possible sources to chose from.
187 *
188 * @return array|bool
189 *
190 * @since 1.0
191 */
192 public static function maybe_source_connection_data()
193 {
194 $nonce = !empty($_GET['cff_con']) ? sanitize_key($_GET['cff_con']) : '';
195 if (!wp_verify_nonce($nonce, 'cff_con')) {
196 return false;
197 }
198
199 if (
200 isset($_GET['cff_access_token'])
201 && isset($_GET['cff_final_response'])
202 && $_GET['cff_final_response'] == 'true'
203 ) {
204 $access_token = sanitize_text_field(wp_unslash($_GET['cff_access_token']));
205 $return = Util::retrieve_available_pages($access_token);
206
207 if ($return) {
208 return $return;
209 } else {
210 return array('error' => __('Unable to connect to Facebook to retrieve account information.', 'reviews-feed'));
211 }
212 }
213 return false;
214 }
215
216 /**
217 * Uses the Facebook API to retrieve a list of pages for the
218 * access token
219 *
220 * @param string $access_token
221 *
222 * @return array|bool
223 *
224 * @since 1.0
225 */
226 public static function retrieve_available_pages($access_token)
227 {
228 //Get User Info
229 $user_url = 'https://graph.facebook.com/me?fields=name,id,picture&access_token=' . urlencode($access_token);
230 $user_id_data = wp_remote_retrieve_body(wp_remote_get($user_url));
231 $user_id_data_arr = json_decode($user_id_data, true);
232
233 $url = 'https://graph.facebook.com/me/accounts?fields=access_token,name,id,overall_star_rating,rating_count&limit=500&access_token=' . urlencode($access_token);
234 $pages_data = wp_remote_retrieve_body(wp_remote_get($url));
235 $pages_data_arr = json_decode($pages_data, true);
236
237
238 if (isset($pages_data_arr['data'])) {
239 $return = [
240 'user' => $user_id_data_arr,
241 'pages' => $pages_data_arr['data'],
242 ];
243
244 return $return;
245 } elseif (isset($pages_data_arr['error'])) {
246 return $pages_data_arr;
247 } else {
248 return [
249 'error' => [
250 'code' => 'HTTP Request',
251 'message' => __('Your server could not complete a remote request to Facebook\'s API. Your host may be blocking access or
252 there may be a problem with your server.', 'reviews-feed')
253 ]
254 ];
255 }
256 }
257
258 public static function currentPageIs($page)
259 {
260 $current_screen = get_current_screen();
261 return $current_screen !== null && !empty($current_screen) && strpos($current_screen->id, $page) !== false;
262 }
263
264 /**
265 * Check Notices for no Provider API KEY
266 *
267 *
268 * @return boolean
269 *
270 * @since 1.0
271 */
272 public static function check_notice_provider_nokey($api_limits)
273 {
274 $noapi_providers = ['wordpress.org', 'trustpilot'];
275 $is_true = $api_limits == $noapi_providers ? false : true;
276
277 foreach ($noapi_providers as $provider) {
278 if ($api_limits == [$provider]) {
279 $is_true = false;
280 }
281 }
282 return $is_true;
283 }
284
285 /**
286 * Returns a list of notices to be displayed in the SB Reviews Pages
287 *
288 *
289 * @return array
290 *
291 * @since 1.0
292 */
293 public static function get_plugin_notices()
294 {
295 $notices = [];
296
297 $api_limits = get_option('sbr_apikeys_limit', []);
298 $should_show_notice = is_array($api_limits) && sizeof($api_limits) > 0 && Util::check_notice_provider_nokey($api_limits);
299 if ($should_show_notice) {
300 array_push($notices, [
301 'type' => 'error',
302 'heading' => __('New reviews will not display until an API key is entered for your sources.', 'reviews-feed'),
303 'description' => __('Due to API limitations your feeds will not show new reviews until you enter an API key on the settings page.', 'reviews-feed'),
304 'actions' => [
305 [
306 'type' => 'primary',
307 'text' => __('How to create an API key', 'reviews-feed'),
308 'link' => admin_url('admin.php?page=sbr-settings')
309 ],
310 [
311 'type' => 'secondary',
312 'text' => __('Go to Settings page', 'reviews-feed'),
313 'link' => admin_url('admin.php?page=sbr-settings')
314 ]
315 ]
316 ]);
317 }
318
319 $noAPIKeyProviders = ['trustpilot', 'wordpress.org'];
320 $noAPIProviderxists = count(array_intersect($noAPIKeyProviders, $api_limits)) ? true : false;
321
322 if ($noAPIProviderxists) {
323 array_push($notices, [
324 'type' => 'error',
325 'heading' => __('You have reached the number of allowed sources.', 'reviews-feed'),
326 'description' => __('Due to API limitations your feeds will not show new reviews because you have reached the number of allowed sources.', 'reviews-feed'),
327 ]);
328 }
329
330 return $notices;
331 }
332
333 /**
334 * Get Smahballoon Plugins Info
335 *
336 * @since 1.0
337 */
338 public static function get_plugins_info()
339 {
340 $installed_plugins = get_plugins();
341 $campaign = self::sbr_is_pro() ? 'reviews-pro' : 'reviews-free';
342
343 $plugins_list = [
344 'facebook' => [
345 'free' => 'custom-facebook-feed/custom-facebook-feed.php',
346 'pro' => 'custom-facebook-feed-pro/custom-facebook-feed.php',
347 'link' => 'https://smashballoon.com/custom-facebook-feed/?utm_campaign=' . $campaign . '&utm_source=about-us&utm_medium=marketing'
348 ],
349 'instagram' => [
350 'free' => 'instagram-feed/instagram-feed.php',
351 'pro' => 'instagram-feed-pro/instagram-feed.php',
352 'link' => 'https://smashballoon.com/instagram-feed/?utm_campaign=' . $campaign . '&utm_source=about-us&utm_medium=marketing'
353 ],
354 'twitter' => [
355 'free' => 'custom-twitter-feeds/custom-twitter-feed.php',
356 'pro' => 'custom-twitter-feeds-pro/custom-twitter-feed.php',
357 'link' => 'https://smashballoon.com/custom-twitter-feeds/?utm_campaign=' . $campaign . '&utm_source=about-us&utm_medium=marketing'
358 ],
359 'youtube' => [
360 'free' => 'feeds-for-youtube/youtube-feed.php',
361 'pro' => 'youtube-feed-pro/youtube-feed.php',
362 'link' => 'https://smashballoon.com/youtube-feed/?utm_campaign=' . $campaign . '&utm_source=about-us&utm_medium=marketing'
363 ]
364 ];
365
366 foreach ($plugins_list as $name => $plugin) {
367 $type = 'none';
368 $activated = 'none';
369 if (isset($installed_plugins[$plugin['free']])) {
370 $type = 'free';
371 $activated = is_plugin_active($plugin['free']);
372 }
373 if (isset($installed_plugins[$plugin['pro']])) {
374 $type = 'pro';
375 $activated = is_plugin_active($plugin['pro']);
376 }
377 $plugins_list[$name]['activated'] = $activated;
378 $plugins_list[$name]['type'] = $type;
379 }
380
381 return [
382 'facebook' => [
383 'plugin' => $plugins_list['facebook']['pro'],
384 'link' => $plugins_list['facebook']['link'],
385 'download_plugin' => 'https://downloads.wordpress.org/plugin/custom-facebook-feed.zip',
386 'title' => __('Custom Facebook Feed', 'reviews-feed'),
387 'description' => __('Add Facebook posts from your timeline, albums and much more.', 'reviews-feed'),
388 'icon' => 'fb-icon.svg',
389 'activated' => $plugins_list['facebook']['activated'],
390 'type' => $plugins_list['facebook']['type'],
391 ],
392 'instagram' => [
393 'plugin' => $plugins_list['instagram']['pro'],
394 'link' => $plugins_list['instagram']['link'],
395 'download_plugin' => 'https://downloads.wordpress.org/plugin/instagram-feed.zip',
396 'title' => __('Instagram Feed', 'reviews-feed'),
397 'description' => __('A quick and elegant way to add your Instagram posts to your website. ', 'reviews-feed'),
398 'icon' => 'insta-icon.svg',
399 'activated' => $plugins_list['instagram']['activated'],
400 'type' => $plugins_list['instagram']['type'],
401 ],
402 'twitter' => [
403 'plugin' => $plugins_list['twitter']['pro'],
404 'link' => $plugins_list['twitter']['link'],
405 'download_plugin' => 'https://downloads.wordpress.org/plugin/custom-twitter-feeds.zip',
406 'title' => __('Custom Twitter Feeds', 'reviews-feed'),
407 'description' => __('A customizable way to display tweets from your Twitter account. ', 'reviews-feed'),
408 'icon' => 'twitter-icon.svg',
409 'activated' => $plugins_list['twitter']['activated'],
410 'type' => $plugins_list['twitter']['type'],
411 ],
412 'youtube' => [
413 'plugin' => $plugins_list['youtube']['pro'],
414 'link' => $plugins_list['youtube']['link'],
415 'download_plugin' => 'https://downloads.wordpress.org/plugin/feeds-for-youtube.zip',
416 'title' => __('Feeds for YouTube', 'reviews-feed'),
417 'description' => __('A simple yet powerful way to display videos from YouTube. ', 'reviews-feed'),
418 'icon' => 'youtube-icon.svg',
419 'activated' => $plugins_list['youtube']['activated'],
420 'type' => $plugins_list['youtube']['type'],
421 ]
422 ];
423 }
424
425 /**
426 * Get Smahballoon Recommended Plugins Info
427 *
428 * @since 1.0
429 */
430 public static function get_smashballoon_recommended_plugins_info()
431 {
432 $installed_plugins = get_plugins();
433 return [
434 'wpforms' => [
435 'plugin' => 'wpforms-lite/wpforms.php',
436 'download_plugin' => 'https://downloads.wordpress.org/plugin/wpforms-lite.zip',
437 'title' => __('WPForms', 'reviews-feed'),
438 'description' => __('The most beginner friendly drag & drop WordPress forms plugin allowing you to create beautiful contact forms, subscription forms, payment forms, and more in minutes, not hours!', 'reviews-feed'),
439 'icon' => 'plugin-wpforms.png',
440 'installed' => isset($installed_plugins['wpforms-lite/wpforms.php']),
441 'activated' => is_plugin_active('wpforms-lite/wpforms.php'),
442 ],
443 'monsterinsights' => [
444 'plugin' => 'google-analytics-for-wordpress/googleanalytics.php',
445 'download_plugin' => 'https://downloads.wordpress.org/plugin/google-analytics-for-wordpress.zip',
446 'title' => __('MonsterInsights', 'reviews-feed'),
447 'description' => __('MonsterInsights makes it “effortless” to properly connect your WordPress site with Google Analytics, so you can start making data-driven decisions to grow your business.', 'reviews-feed'),
448 'icon' => 'plugin-mi.png',
449 'installed' => isset($installed_plugins['google-analytics-for-wordpress/googleanalytics.php']),
450 'activated' => is_plugin_active('google-analytics-for-wordpress/googleanalytics.php'),
451 ],
452 'optinmonster' => [
453 'plugin' => 'optinmonster/optin-monster-wp-api.php',
454 'download_plugin' => 'https://downloads.wordpress.org/plugin/optinmonster.zip',
455 'title' => __('OptinMonster', 'reviews-feed'),
456 'description' => __('Our high-converting optin forms like Exit-Intent® popups, Fullscreen Welcome Mats, and Scroll boxes help you dramatically boost conversions and get more email subscribers.', 'reviews-feed'),
457 'icon' => 'plugin-om.png',
458 'installed' => isset($installed_plugins['optinmonster/optin-monster-wp-api.php']),
459 'activated' => is_plugin_active('optinmonster/optin-monster-wp-api.php'),
460 ],
461 'wp_mail_smtp' => [
462 'plugin' => 'wp-mail-smtp/wp_mail_smtp.php',
463 'download_plugin' => 'https://downloads.wordpress.org/plugin/wp-mail-smtp.zip',
464 'title' => __('WP Mail SMTP', 'reviews-feed'),
465 'description' => __('Make sure your website\'s emails reach the inbox. Our goal is to make email deliverability easy and reliable. Trusted by over 1 million websites.', 'reviews-feed'),
466 'icon' => 'plugin-smtp.png',
467 'installed' => isset($installed_plugins['wp-mail-smtp/wp_mail_smtp.php']),
468 'activated' => is_plugin_active('wp-mail-smtp/wp_mail_smtp.php'),
469 ],
470 'rafflepress' => [
471 'plugin' => 'rafflepress/rafflepress.php',
472 'download_plugin' => 'https://downloads.wordpress.org/plugin/rafflepress.zip',
473 'title' => __('RafflePress', 'reviews-feed'),
474 'description' => __('Turn your visitors into brand ambassadors! Easily grow your email list, website traffic, and social media followers with powerful viral giveaways & contests.', 'reviews-feed'),
475 'icon' => 'plugin-rp.png',
476 'installed' => isset($installed_plugins['rafflepress/rafflepress.php']),
477 'activated' => is_plugin_active('rafflepress/rafflepress.php'),
478 ],
479 'aioseo' => [
480 'plugin' => 'all-in-one-seo-pack/all_in_one_seo_pack.php',
481 'download_plugin' => 'https://downloads.wordpress.org/plugin/all-in-one-seo-pack.zip',
482 'title' => __('All in One SEO Pack', 'reviews-feed'),
483 'description' => __('Out-of-the-box SEO for WordPress. Features like XML Sitemaps, SEO for custom post types, SEO for blogs, business sites, or ecommerce sites, and much more.', 'reviews-feed'),
484 'icon' => 'plugin-seo.png',
485 'installed' => isset($installed_plugins['all-in-one-seo-pack/all_in_one_seo_pack.php']),
486 'activated' => is_plugin_active('all-in-one-seo-pack/all_in_one_seo_pack.php'),
487 ]
488 ];
489 }
490 /**
491 * Get other active plugins of Smash Balloon
492 *
493 * @since 4.4.0
494 */
495 public static function get_sb_active_plugins_info()
496 {
497 // get the WordPress's core list of installed plugins
498 if (!function_exists('get_plugins')) {
499 require_once ABSPATH . 'wp-admin/includes/plugin.php';
500 }
501 $installed_plugins = get_plugins();
502
503 $is_facebook_installed = false;
504 $facebook_plugin = 'custom-facebook-feed/custom-facebook-feed.php';
505 if (isset($installed_plugins['custom-facebook-feed-pro/custom-facebook-feed.php'])) {
506 $is_facebook_installed = true;
507 $facebook_plugin = 'custom-facebook-feed-pro/custom-facebook-feed.php';
508 } elseif (isset($installed_plugins['custom-facebook-feed/custom-facebook-feed.php'])) {
509 $is_facebook_installed = true;
510 }
511
512 $is_instagram_installed = false;
513 $instagram_plugin = 'instagram-feed/instagram-feed.php';
514 if (isset($installed_plugins['instagram-feed-pro/instagram-feed.php'])) {
515 $is_instagram_installed = true;
516 $instagram_plugin = 'instagram-feed-pro/instagram-feed.php';
517 } elseif (isset($installed_plugins['instagram-feed/instagram-feed.php'])) {
518 $is_instagram_installed = true;
519 }
520
521 $is_twitter_installed = false;
522 $twitter_plugin = 'custom-twitter-feeds/custom-twitter-feed.php';
523 if (isset($installed_plugins['custom-twitter-feeds-pro/custom-twitter-feed.php'])) {
524 $is_twitter_installed = true;
525 $twitter_plugin = 'custom-twitter-feeds-pro/custom-twitter-feed.php';
526 } elseif (isset($installed_plugins['custom-twitter-feeds/custom-twitter-feed.php'])) {
527 $is_twitter_installed = true;
528 }
529
530 $is_youtube_installed = false;
531 $youtube_plugin = 'feeds-for-youtube/youtube-feed.php';
532 if (isset($installed_plugins['youtube-feed-pro/youtube-feed-pro.php'])) {
533 $is_youtube_installed = true;
534 $youtube_plugin = 'youtube-feed-pro/youtube-feed-pro.php';
535 } elseif (isset($installed_plugins['feeds-for-youtube/youtube-feed.php'])) {
536 $is_youtube_installed = true;
537 }
538
539 $is_social_wall_installed = isset($installed_plugins['social-wall/social-wall.php']) ? true : false;
540 $social_wall_plugin = 'social-wall/social-wall.php';
541
542
543 return array(
544 'is_facebook_installed' => $is_facebook_installed,
545 'is_instagram_installed' => $is_instagram_installed,
546 'is_twitter_installed' => $is_twitter_installed,
547 'is_youtube_installed' => $is_youtube_installed,
548 'is_social_wall_installed' => $is_social_wall_installed,
549 'facebook_plugin' => $facebook_plugin,
550 'instagram_plugin' => $instagram_plugin,
551 'twitter_plugin' => $twitter_plugin,
552 'youtube_plugin' => $youtube_plugin,
553 'social_wall_plugin' => $social_wall_plugin,
554 'installed_plugins' => $installed_plugins
555 );
556 }
557
558 /**
559 * SBR Get Whitespace
560 *
561 * @since 1.0
562 *
563 * @param int $times
564 *
565 * @return string
566 */
567 public static function get_whitespace($times)
568 {
569 return str_repeat('&nbsp;', $times);
570 }
571
572 /**
573 * Get Site and Server Info
574 *
575 * @since 1.0
576 *
577 * @return string
578 */
579 public static function get_site_n_server_info()
580 {
581 $allow_url_fopen = ini_get('allow_url_fopen') ? "Yes" : "No";
582 $php_curl = is_callable('curl_init') ? "Yes" : "No";
583 $php_json_decode = function_exists("json_decode") ? "Yes" : "No";
584 $php_ssl = in_array('https', stream_get_wrappers()) ? "Yes" : "No";
585
586 $output = '## SITE/SERVER INFO: ##' . "</br>";
587 $output .= 'Plugin Version:' . self::get_whitespace(11) . SBR_MENU_SLUG . "</br>";
588 $output .= 'Site URL:' . self::get_whitespace(17) . site_url() . "</br>";
589 $output .= 'Home URL:' . self::get_whitespace(17) . home_url() . "</br>";
590 $output .= 'WordPress Version:' . self::get_whitespace(8) . get_bloginfo('version') . "</br>";
591 $output .= 'PHP Version:' . self::get_whitespace(14) . PHP_VERSION . "</br>";
592 $output .= 'Web Server Info:' . self::get_whitespace(10) . esc_html($_SERVER['SERVER_SOFTWARE']) . "</br>";
593 $output .= 'PHP allow_url_fopen:' . self::get_whitespace(6) . $allow_url_fopen . "</br>";
594 $output .= 'PHP cURL:' . self::get_whitespace(17) . $php_curl . "</br>";
595 $output .= 'JSON:' . self::get_whitespace(21) . $php_json_decode . "</br>";
596 $output .= 'SSL Stream:' . self::get_whitespace(15) . $php_ssl . "</br>";
597 $output .= "</br>";
598
599 return $output;
600 }
601
602 /**
603 * Get Active Plugins
604 *
605 * @since 1.0
606 *
607 * @return string
608 */
609 public static function get_active_plugins_info()
610 {
611 $plugins = get_plugins();
612 $active_plugins = get_option('active_plugins');
613 $output = "## ACTIVE PLUGINS: ## </br>";
614
615 foreach ($plugins as $plugin_path => $plugin) {
616 if (in_array($plugin_path, $active_plugins)) {
617 $output .= $plugin['Name'] . ': ' . $plugin['Version'] . "</br>";
618 }
619 }
620
621 $output .= "</br>";
622
623 return $output;
624 }
625
626
627 /**
628 * Get Global Settings
629 *
630 * @since 1.0
631 *
632 * @return string
633 */
634 public static function get_global_settings_info()
635 {
636 $output = '## GLOBAL SETTINGS: ## </br>';
637 $sbr_settings = get_option('sbr_settings', array());
638 if (! is_array($sbr_settings)) {
639 $sbr_settings = array();
640 }
641
642 $plugin_status = new AuthorizationStatusCheck();
643
644 $statuses = $plugin_status->get_statuses();
645
646 if (Util::sbr_is_pro()) {
647 $output .= 'License key: ';
648 if (isset($sbr_settings['license_key'])) {
649 $output .= esc_html($sbr_settings['license_key']);
650 } else {
651 $output .= ' Not added';
652 }
653
654 $output .= '</br>';
655 $output .= 'License Tier: ';
656 if (isset($statuses['license_tier'])) {
657 $output .= esc_html($statuses['license_tier']);
658 } else {
659 $output .= ' Not Set';
660 }
661 $output .= '</br>';
662 $output .= 'License status: ';
663 if (isset($sbr_settings['license_status'])) {
664 $output .= $sbr_settings['license_status'];
665 } else {
666 $output .= ' Inactive';
667 }
668 $output .= '</br>';
669 }
670 $output .= 'Preserve settings if plugin is removed: ';
671 $output .= isset($sbr_settings['preserve_settings']) && ($sbr_settings['preserve_settings']) ? 'Yes' : 'No';
672 $output .= '</br>';
673 $output .= 'Caching: ';
674 $output .= $statuses['license_tier'] === 3 ? 'Twice daily' : 'daily';
675
676 $output .= '</br>';
677 $output .= 'GDPR: ';
678 $output .= isset($sbr_settings['gdpr']) ? $sbr_settings['gdpr'] : ' Not setup';
679 $output .= '</br>';
680 $output .= 'Optimize Images: ';
681 $output .= isset($sbr_settings['optimize_images']) && $sbr_settings['optimize_images'] === true ? 'Enabled' : 'Disabled';
682 $output .= '</br>';
683 $output .= 'Usage Tracking: ';
684 $output .= isset($sbr_settings['usagetracking']) && $sbr_settings['usagetracking'] === true ? 'Enabled' : 'Disabled';
685 $output .= '</br>';
686 $output .= 'Enqueue in Head: ';
687 $output .= isset($sbr_settings['enqueue_js_in_header']) && $sbr_settings['enqueue_js_in_header'] === true ? 'Enabled' : 'Disabled';
688 $output .= '</br>';
689 $output .= 'Admin Error Notice: ';
690 $output .= isset($sbr_settings['admin_error_notices']) && $sbr_settings['admin_error_notices'] === true ? 'Enabled' : 'Disabled';
691 $output .= '</br>';
692 $output .= 'Feed Issue Email Reports: ';
693 $output .= isset($sbr_settings['feed_issue_reports']) && $sbr_settings['feed_issue_reports'] === true ? 'Enabled' : 'Disabled';
694 $output .= '</br>';
695 $output .= '</br>';
696 return $output;
697 }
698
699 /**
700 * Get Feeds Settings
701 *
702 * @since 1.0
703 *
704 * @return string
705 */
706 public static function get_sources_settings_info()
707 {
708 $output = '## SOURCES: ## </br>';
709 $source_list = SBR_Sources::get_sources_list();
710 foreach ($source_list as $feed) {
711 $output .= $feed['name'] . ' ( ' . strtoupper($feed['provider']) . ' => ' . $feed['account_id'] . ' )';
712 $output .= '</br>';
713 }
714 $output .= '</br>';
715
716 return $output;
717 }
718
719 /**
720 * Get Feeds Settings
721 *
722 * @since 1.0
723 *
724 * @return string
725 */
726 public static function get_api_settings_info()
727 {
728 $output = '## API KEYS: ## </br>';
729 $api_keys = get_option('sbr_apikeys', []);
730
731 foreach ($api_keys as $id => $api) {
732 $output .= ucfirst($id) . ' => ' . $api;
733 $output .= '</br>';
734 }
735 $output .= '</br>';
736
737 return $output;
738 }
739
740
741
742 /**
743 * Get Feeds Settings
744 *
745 * @since 1.0
746 *
747 * @return string
748 */
749 public static function get_feeds_settings_info()
750 {
751 $output = '## FEEDS: ## </br>';
752
753 $feeds_list = DB::get_feeds_list();
754
755 $i = 0;
756 foreach ($feeds_list as $feed) {
757 if ($i >= 25) {
758 break;
759 }
760 $output .= $feed['feed_name'];
761 if (isset($feed['settings'])) {
762 $output .= '</br>';
763 if (!empty($feed['sourcesList'])) {
764 foreach ($feed['sourcesList'] as $id => $source) {
765 $output .= esc_html($source['name']);
766 $output .= ' (' . esc_html(ucfirst($source['name'])) . ' => ' . esc_html($source['account_id']) . ')';
767 $output .= '</br>';
768 }
769 }
770 }
771 $output .= '</br>';
772 if (isset($feed['location_summary']) && count($feed['location_summary']) > 0) {
773 $first_feed = $feed['location_summary'][0];
774 if (!empty($first_feed['link'])) {
775 $output .= esc_html($first_feed['link']) . '?sb_debug';
776 $output .= '</br>';
777 }
778 }
779
780 if ($i < (count($feeds_list) - 1)) {
781 $output .= '</br>';
782 }
783 $i++;
784 }
785 $output .= '</br>';
786
787 return $output;
788 }
789
790 /**
791 * Get Posts Table Info
792 *
793 * @since 1.0
794 *
795 * @return string
796 */
797 public static function get_posts_table_info()
798 {
799 $output = '## POSTS: ## </br>';
800
801 global $wpdb;
802 $table_name = $wpdb->prefix . 'sbr_posts';
803 $feeds_posts_table_name = $wpdb->prefix . 'sbr_reviews_posts';
804
805 if ($wpdb->get_var("show tables like '$feeds_posts_table_name'") !== $feeds_posts_table_name) {
806 $output .= 'no feeds posts table</br>';
807 } else {
808 $last_result = $wpdb->get_results("SELECT * FROM $feeds_posts_table_name ORDER BY id DESC LIMIT 1;");
809 if (is_array($last_result) && isset($last_result[0])) {
810 $output .= '## FEEDS POSTS TABLE ##</br>';
811 foreach ($last_result as $column) {
812 foreach ($column as $key => $value) {
813 $output .= esc_html($key) . ': ' . esc_html($value) . '</br>';
814 }
815 }
816 } else {
817 $output .= 'feeds posts has no rows';
818 $output .= '</br>';
819 }
820 }
821 $output .= '</br>';
822 if ($wpdb->get_var("show tables like '$table_name'") !== $table_name) {
823 $output .= 'no posts table</br>';
824 } else {
825 $last_result = $wpdb->get_results("SELECT * FROM $table_name ORDER BY id DESC LIMIT 1;");
826 if (is_array($last_result) && isset($last_result[0])) {
827 $output .= '## POSTS TABLE ##';
828 $output .= '</br>';
829 foreach ($last_result as $column) {
830 foreach ($column as $key => $value) {
831 $output .= esc_html($key) . ': ' . esc_html($value) . '</br>';
832 }
833 }
834 } else {
835 $output .= 'posts has no rows</br>';
836 }
837 }
838 $output .= '</br>';
839
840 return $output;
841 }
842
843 /**
844 * List of possible languages/translations
845 *
846 * @since 1.0
847 *
848 * @return array
849 */
850 public static function get_translation_languages($include_default = false)
851 {
852 $languages = [
853 'default' => 'Default',
854 '' => 'No Translation',
855 'en' => 'English',
856 'af' => 'Afrikaans',
857 'am' => 'Amharic',
858 'ar' => 'Arabic',
859 'az' => 'Azerbaijani',
860 'be' => 'Belarusian',
861 'bg' => 'Bulgarian',
862 'bn' => 'Bengali',
863 'bs' => 'Bosnian',
864 'ca' => 'Catalan',
865 'cs' => 'Czech',
866 'da' => 'Danish',
867 'de' => 'German',
868 'el' => 'Greek',
869 'en-AU' => 'English (Australian)',
870 'en-GB' => 'English (Great Britain)',
871 'es' => 'Spanish',
872 'es-419' => 'Spanish (Latin America)',
873 'et' => 'Estonian',
874 'eu' => 'Basque',
875 'fa' => 'Farsi',
876 'fi' => 'Finnish',
877 'fil' => 'Filipino',
878 'fr' => 'French',
879 'fr-CA' => 'French (Canada)',
880 'gl' => 'Galician',
881 'gu' => 'Gujarati',
882 'hi' => 'Hindi',
883 'hr' => 'Croatian',
884 'hu' => 'Hungarian',
885 'hy' => 'Armenian',
886 'id' => 'Indonesian',
887 'is' => 'Icelandic',
888 'it' => 'Italian ',
889 'iw' => 'Hebrew',
890 'ja' => 'Japanese',
891 'ka' => 'Georgian',
892 'kk' => 'Kazakh',
893 'km' => 'Khmer',
894 'kn' => 'Kannada',
895 'ko' => 'Korean',
896 'ky' => 'Kyrgyz',
897 'lo' => 'Lao',
898 'lt' => 'Lithuanian',
899 'lv' => 'Latvian',
900 'mk' => 'Macedonian',
901 'ml' => 'Malayalam',
902 'mn' => 'Mongolian',
903 'mr' => 'Marathi',
904 'ms' => 'Malay',
905 'my' => 'Burmese',
906 'ne' => 'Nepali',
907 'nl' => 'Dutch',
908 'no' => 'Norwegian',
909 'pa' => 'Punjabi',
910 'pl' => 'Polish',
911 'pt' => 'Portuguese',
912 'pt-BR' => 'Portuguese (Brazil)',
913 'pt-PT' => 'Portuguese (Portugal)',
914 'ro' => 'Romanian',
915 'ru' => 'Russian',
916 'si' => 'Sinhalese',
917 'sk' => 'Slovak',
918 'sl' => 'Slovenian',
919 'sq' => 'Albanian',
920 'sr' => 'Serbian',
921 'sv' => 'Swedish',
922 'sw' => 'Swahili',
923 'ta' => 'Tamil',
924 'te' => 'Telugu',
925 'th' => 'Thai',
926 'tr' => 'Turkish',
927 'uk' => 'Ukrainian',
928 'ur' => 'Urdu',
929 'uz' => 'Uzbek',
930 'vi' => 'Vietnamese',
931 'zh' => 'Chinese',
932 'zh-CN' => 'Chinese (Simplified)',
933 'zh-HK' => 'Chinese (Hong Kong)',
934 'zh-TW' => 'Chinese (Traditional)',
935 'zu' => 'Zulu'
936 ];
937 if ($include_default === false) {
938 array_shift($languages);
939 }
940
941 //Detect if WPMl is active then add the option
942 if (defined('ICL_SITEPRESS_VERSION') && Util::sbr_is_pro()) {
943 $position = $include_default ? 2 : 1;
944 $languages = array_merge(
945 array_slice($languages, 0, $position),
946 ['wpml' => 'Automatically by WPML'],
947 array_slice($languages, $position)
948 );
949 }
950
951 return $languages;
952 }
953
954 /**
955 * Get Language for API call
956 *
957 * @since 1.0
958 *
959 * @return string
960 */
961
962 public static function get_settings_language($settings)
963 {
964 $args = isset($settings['localization']) && $settings['localization'] !== 'default'
965 ? $settings
966 : wp_parse_args(get_option('sbr_settings', []), sbr_plugin_settings_defaults());
967
968 return $args['localization'];
969 }
970
971 /**
972 * Get Language for API call
973 *
974 * @since 1.0
975 *
976 * @return string
977 */
978 public static function get_api_call_language($settings)
979 {
980 $language = Util::sbr_is_pro()
981 ? \SmashBalloon\Reviews\Pro\Helpers\SBR_WPML::get_current_language(Util::get_settings_language($settings))
982 : Util::get_settings_language($settings);
983
984 /**
985 * Filter the language code sent to the provider API (e.g. Google Places).
986 * Lets a site map an unsupported locale to a supported one, e.g. a WPML
987 * `es-mx` site forcing `es-419` for Latin American Spanish. SMASH-1617.
988 * Read the current WPML language inside the callback (apply_filters
989 * 'wpml_current_language') if you need per-page context.
990 *
991 * @since 2.6.6
992 * @param string $language Resolved language code (or 'default').
993 */
994 return apply_filters('sbr_google_api_language', $language);
995 }
996
997 /**
998 * Map a locale / WPML language code to a code the Google Places API accepts.
999 *
1000 * Google only honours codes from its supported list (get_translation_languages);
1001 * an unsupported value (e.g. WPML's `es-mx`) makes Google return reviews
1002 * untranslated. This normalises common regional codes to a supported one.
1003 * Spanish is special-cased: Spain dialects -> `es`, any other Spanish variant
1004 * (es-mx, es-ar, …) -> `es-419` (Latin America) — region-stripping alone would
1005 * wrongly land on Spain Spanish. Returns null when nothing supported matches,
1006 * so callers can fall back (never send a bogus code). SMASH-1617.
1007 *
1008 * @since 2.6.6
1009 * @param string|null $code Raw locale / WPML code (e.g. 'es-mx', 'pt-br', 'es_MX').
1010 * @return string|null A Google-supported code, or null if unmappable.
1011 */
1012 public static function map_wpml_to_google_language($code)
1013 {
1014 if (! is_string($code) || $code === '') {
1015 return null;
1016 }
1017
1018 $allowed = self::get_translation_languages();
1019
1020 // Already a supported code (exact match) — keep existing behaviour.
1021 if (isset($allowed[$code]) && $code !== 'default' && $code !== '') {
1022 return $code;
1023 }
1024
1025 $norm = strtolower(str_replace('_', '-', $code));
1026
1027 // Spanish: Spain -> es; every other variant (es-mx, es-ar, …) -> es-419.
1028 if ($norm === 'es' || $norm === 'es-es') {
1029 return 'es';
1030 }
1031 if (strpos($norm, 'es-') === 0) {
1032 return 'es-419';
1033 }
1034
1035 // Case-insensitive match for region variants Google does list (pt-br -> pt-BR).
1036 foreach (array_keys($allowed) as $supported) {
1037 if (strtolower($supported) === $norm) {
1038 return $supported;
1039 }
1040 }
1041
1042 // Strip the region and fall back to the base language if supported (fr-fr -> fr).
1043 $base = strtok($norm, '-');
1044 if ($base !== false && isset($allowed[$base])) {
1045 return $base;
1046 }
1047
1048 return null;
1049 }
1050
1051 /**
1052 * Is Plugin Pro
1053 *
1054 * @since 1.0
1055 *
1056 * @return boolean
1057 */
1058 public static function sbr_is_pro()
1059 {
1060 return defined('SBR_PRO') && SBR_PRO === true;
1061 }
1062
1063 /**
1064 * Check if user has Pro Plus tier
1065 *
1066 * @since 2.5.0
1067 *
1068 * @return boolean
1069 */
1070 public static function sbr_is_pro_plus()
1071 {
1072 if (!self::sbr_is_pro()) {
1073 return false;
1074 }
1075
1076 // Read from sbr_statuses where license_tier is written during license activation
1077 // This is consistent with AuthorizationStatusCheck::get_license_tier()
1078 $statuses = get_option('sbr_statuses', []);
1079 $license_tier = $statuses['license_tier'] ?? 0;
1080
1081 // Handle All Access Bundle special case (same as AuthorizationStatusCheck)
1082 if (
1083 isset($statuses['license_info']['item_name'])
1084 && $statuses['license_info']['item_name'] === 'All Access Bundle - All Plugins Unlimited'
1085 ) {
1086 $license_tier = 3;
1087 }
1088
1089 // Tiers: 1 = Pro Basic, 2 = Pro Plus, 3 = Elite
1090 // Pro Plus and Elite (tiers 2 and 3) have full access
1091 return (int) $license_tier >= 2;
1092 }
1093
1094
1095 /**
1096 * Get List of Upsell Modal Content
1097 *
1098 * @since 1.0
1099 *
1100 * @return array
1101 */
1102 public static function upsell_modal_content()
1103 {
1104 // Pro users (including Pro Plus) don't need provider upsell modals
1105 // Note: Review Alert Pro Plus upsells are handled separately in SBR_ReviewAlert_Builder
1106 if (Util::sbr_is_pro()) {
1107 return [];
1108 }
1109
1110 // Free users get all upsell modals
1111 return [
1112 'facebookProvider' => [
1113 'heading' => __('Upgrade to Pro to display Facebook reviews', 'reviews-feed'),
1114 'description' => __('Upgrade to our "Plus" tier to display reviews from the well known social media platform.', 'reviews-feed'),
1115 'image' => 'upsell-facebook.png',
1116 'buttons' => [
1117 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=facebook-modal&utm_content=LiteUsers50OFF',
1118 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=facebook-modal&utm_content=Upgrade',
1119 # 'demo' => 'https://smashballoon.com/reviews-feed/demo/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=responsive-modal&utm_content=ViewDemo'
1120 ],
1121
1122 'includeContent' => true
1123 ],
1124 'trustpilotProvider' => [
1125 'heading' => __('Upgrade to Pro to display TrustPilot reviews', 'reviews-feed'),
1126 'description' => __('Upgrade to our "Plus" tier to display reviews from the well known business review site.', 'reviews-feed'),
1127 'image' => 'upsell-trustpilot.png',
1128 'buttons' => [
1129 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=trustpilot-modal&utm_content=LiteUsers50OFF',
1130 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=trustpilot-modal&utm_content=Upgrade'
1131 ],
1132 'includeContent' => true
1133 ],
1134 'tripadvisorProvider' => [
1135 'heading' => __('Upgrade to Pro to display TripAdvisor reviews', 'reviews-feed'),
1136 'description' => __('Upgrade to our "Elite" tier to display reviews from the well known travel advice site.', 'reviews-feed'),
1137 'image' => 'upsell-tripadvisor.png',
1138 'buttons' => [
1139 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=tripadvisor-modal&utm_content=LiteUsers50OFF',
1140 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=tripadvisor-modal&utm_content=Upgrade'
1141 ],
1142 'includeContent' => true
1143 ],
1144 'wordpress.orgProvider' => [
1145 'heading' => __('Upgrade to Pro to display WordPress.org reviews', 'reviews-feed'),
1146 'description' => __('Upgrade to our "Elite" tier to display reviews for plugins and themes.', 'reviews-feed'),
1147 'image' => 'upsell-wordpress.org.png',
1148 'buttons' => [
1149 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=wordpressorg-modal&utm_content=LiteUsers50OFF',
1150 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=wordpressorg-modal&utm_content=Upgrade'
1151 ],
1152 'includeContent' => true
1153 ],
1154 'woocommerceProvider' => [
1155 'heading' => __('Upgrade to Pro to display WooCommerce reviews', 'reviews-feed'),
1156 'description' => __('Upgrade to our "Plus" tier to display product reviews from your WooCommerce store.', 'reviews-feed'),
1157 'image' => 'upsell-woocommerce.png',
1158 'buttons' => [
1159 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=woocommerce-modal&utm_content=LiteUsers50OFF',
1160 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=woocommerce-modal&utm_content=Upgrade'
1161 ],
1162 'includeContent' => true
1163 ],
1164 'eddProvider' => [
1165 'heading' => __('Upgrade to Pro to display Easy Digital Downloads reviews', 'reviews-feed'),
1166 'description' => __('Upgrade to our "Plus" tier to display download reviews from your EDD store.', 'reviews-feed'),
1167 'image' => 'upsell-woocommerce.png',
1168 'buttons' => [
1169 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=edd-modal&utm_content=LiteUsers50OFF',
1170 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=edd-modal&utm_content=Upgrade'
1171 ],
1172 'includeContent' => true
1173 ],
1174 'airbnbProvider' => [
1175 'heading' => __('Upgrade to Pro to display Airbnb reviews', 'reviews-feed'),
1176 'description' => __('Upgrade to our "Plus" tier to display reviews from the popular accommodation platform.', 'reviews-feed'),
1177 'image' => 'upsell-airbnb.png',
1178 'buttons' => [
1179 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=airbnb-modal&utm_content=LiteUsers50OFF',
1180 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=airbnb-modal&utm_content=Upgrade'
1181 ],
1182 'includeContent' => true
1183 ],
1184 'bookingProvider' => [
1185 'heading' => __('Upgrade to Pro to display Booking.com reviews', 'reviews-feed'),
1186 'description' => __('Upgrade to our "Plus" tier to display reviews from the leading travel booking site.', 'reviews-feed'),
1187 'image' => 'upsell-booking.png',
1188 'buttons' => [
1189 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=booking-modal&utm_content=LiteUsers50OFF',
1190 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=booking-modal&utm_content=Upgrade'
1191 ],
1192 'includeContent' => true
1193 ],
1194 'aliexpressProvider' => [
1195 'heading' => __('Upgrade to Pro to display AliExpress reviews', 'reviews-feed'),
1196 'description' => __('Upgrade to our "Plus" tier to display product reviews from the global marketplace.', 'reviews-feed'),
1197 'image' => 'upsell-aliexpress.png',
1198 'buttons' => [
1199 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=aliexpress-modal&utm_content=LiteUsers50OFF',
1200 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=aliexpress-modal&utm_content=Upgrade'
1201 ],
1202 'includeContent' => true
1203 ],
1204 'carouselModal' => [
1205 'heading' => __('Upgrade to Pro to get Carousel layout', 'reviews-feed'),
1206 'description' => __('An eye-catching rotating slider of your videos to add extra content in minimal space on your website.', 'reviews-feed'),
1207 'image' => 'upsell-carousel.png',
1208 'buttons' => [
1209 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=carousel-modal&utm_content=LiteUsers50OFF',
1210 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=carousel-modal&utm_content=Upgrade'
1211 ],
1212 'includeContent' => true
1213 ],
1214 'moreReviewsModal' => [
1215 'heading' => __('Upgrade to Pro to display more reviews', 'reviews-feed'),
1216 'description' => __('More layout settings to customize the look and feel of your reviews even more.', 'reviews-feed'),
1217 'image' => 'upsell-morereviews.png',
1218 'buttons' => [
1219 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=num-reviews-modal&utm_content=LiteUsers50OFF',
1220 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=num-reviews-modal&utm_content=Upgrade'
1221 ],
1222 'includeContent' => true
1223 ],
1224 'averageRatingModal' => [
1225 'heading' => __('Upgrade to Pro to display average rating', 'reviews-feed'),
1226 'description' => __('Boost social proof to make more sales conversions with the number of ratings and an average rating.', 'reviews-feed'),
1227 'image' => 'upsell-averagerating.png',
1228 'buttons' => [
1229 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=average-rating-modal&utm_content=LiteUsers50OFF',
1230 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=average-rating-modal&utm_content=Upgrade'
1231 ],
1232 'includeContent' => true
1233 ],
1234 'loadMoreModal' => [
1235 'heading' => __('Upgrade to Pro to add load more functionality', 'reviews-feed'),
1236 'description' => __('Overwhelm (in a good way) your visitors with additional reviews loaded on the page with a click.', 'reviews-feed'),
1237 'image' => 'upsell-loadmore.png',
1238 'buttons' => [
1239 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=load-more-modal&utm_content=LiteUsers50OFF',
1240 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=load-more-modal&utm_content=Upgrade'
1241 ],
1242 'includeContent' => true
1243 ],
1244 'reviewsMediaModal' => [
1245 'heading' => __('Upgrade to Pro to add images', 'reviews-feed'),
1246 'description' => __('Display images from Yelp and Tripadvisor reviews.', 'reviews-feed'),
1247 'image' => 'upsell-reviewsmedia.png',
1248 'buttons' => [
1249 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=lite-upgrade-footer-coupon&utm_content=LiteUsers50OFF',
1250 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=template-modal&utm_content=Upgrade'
1251 ],
1252 'includeContent' => true
1253 ],
1254 'authorImageModal' => [
1255 'heading' => __('Upgrade to Pro to display author images', 'reviews-feed'),
1256 'description' => __('Build brand trust with positive reviews from real customers.', 'reviews-feed'),
1257 'image' => 'upsell-authorimage.png',
1258 'buttons' => [
1259 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=author-avatar-modal&utm_content=LiteUsers50OFF',
1260 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=author-avatar-modal&utm_content=Upgrade'
1261 ],
1262 'includeContent' => true
1263 ],
1264 'filtersModal' => [
1265 'heading' => __('Upgrade to Pro to filter your reviews', 'reviews-feed'),
1266 'description' => __('Show only the most positive reviews and build brand trust with review filtering.', 'reviews-feed'),
1267 'image' => 'upsell-filters.png',
1268 'buttons' => [
1269 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=star-filter-modal&utm_content=LiteUsers50OFF',
1270 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=star-filter-modal&utm_content=Upgrade'
1271 ],
1272 'includeContent' => true
1273 ],
1274 'moderationModal' => [
1275 'heading' => __('Upgrade to Pro to moderate your reviews', 'reviews-feed'),
1276 'description' => __('Take complete control of what reviews show in the feed using keyword filters and a visual moderation system.', 'reviews-feed'),
1277 'image' => 'upsell-moderation.png',
1278 'buttons' => [
1279 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=moderation-modal&utm_content=LiteUsers50OFF',
1280 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=moderation-modal&utm_content=Upgrade'
1281 ],
1282 'includeContent' => true
1283 ],
1284 'templateModal' => [
1285 'heading' => __('Upgrade to Pro to get one-click templates!', 'reviews-feed'),
1286 'description' => __('Quickly create and preview new feeds with pre-configured options based on popular feed types.', 'reviews-feed'),
1287 'image' => 'upsell-template.png',
1288 'buttons' => [
1289 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=template-modal&utm_content=LiteUsers50OFF',
1290 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=template-modal&utm_content=Upgrade'
1291 ],
1292 'includeContent' => true
1293 ],
1294 'responsiveModal' => [
1295 'heading' => __('Upgrade to Pro for responsive layouts', 'reviews-feed'),
1296 'description' => __('Take control of your feed layouts by customizing number of reviews & columns', 'reviews-feed'),
1297 'image' => 'upsell-responsive.png',
1298 'buttons' => [
1299 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=responsive-modal&utm_content=LiteUsers50OFF',
1300 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=responsive-modal&utm_content=Upgrade'
1301 ],
1302 'includeContent' => true
1303 ],
1304 // Review Alert Upsell Modals
1305 'reviewAlertFeature' => [
1306 'heading' => __('Upgrade to Pro for Review Alerts', 'reviews-feed'),
1307 'description' => __('Display eye-catching review popups that cycle through your best reviews to boost social proof and conversions.', 'reviews-feed'),
1308 'image' => 'upsell-review-alert.png',
1309 'buttons' => [
1310 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-feature-modal&utm_content=LiteUsers50OFF',
1311 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-feature-modal&utm_content=Upgrade'
1312 ],
1313 'includeContent' => true
1314 ],
1315 'reviewAlertRecentReviews' => [
1316 'heading' => __('Upgrade to Pro for Recent Reviews popup', 'reviews-feed'),
1317 'description' => __('Cycle through your individual reviews one by one to showcase real customer feedback and build trust.', 'reviews-feed'),
1318 'image' => 'upsell-review-alert.png',
1319 'buttons' => [
1320 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-recent-reviews-modal&utm_content=LiteUsers50OFF',
1321 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-recent-reviews-modal&utm_content=Upgrade'
1322 ],
1323 'includeContent' => true
1324 ],
1325 'reviewAlertVariations' => [
1326 'heading' => __('Upgrade to Pro for more popup styles', 'reviews-feed'),
1327 'description' => __('Unlock V2 and V3 style variations for your notification popups with unique layouts and designs.', 'reviews-feed'),
1328 'image' => 'upsell-review-alert.png',
1329 'buttons' => [
1330 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-variations-modal&utm_content=LiteUsers50OFF',
1331 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-variations-modal&utm_content=Upgrade'
1332 ],
1333 'includeContent' => true
1334 ],
1335 'reviewAlertDarkTheme' => [
1336 'heading' => __('Upgrade to Pro for dark theme popups', 'reviews-feed'),
1337 'description' => __('Match your site\'s dark mode with elegant dark-themed notification popups.', 'reviews-feed'),
1338 'image' => 'upsell-review-alert.png',
1339 'buttons' => [
1340 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-dark-theme-modal&utm_content=LiteUsers50OFF',
1341 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-dark-theme-modal&utm_content=Upgrade'
1342 ],
1343 'includeContent' => true
1344 ],
1345 'reviewAlertCustomColor' => [
1346 'heading' => __('Upgrade to Pro for custom popup colors', 'reviews-feed'),
1347 'description' => __('Customize your popup accent color to perfectly match your brand identity.', 'reviews-feed'),
1348 'image' => 'upsell-review-alert.png',
1349 'buttons' => [
1350 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-custom-color-modal&utm_content=LiteUsers50OFF',
1351 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-custom-color-modal&utm_content=Upgrade'
1352 ],
1353 'includeContent' => true
1354 ],
1355 'reviewAlertMultiple' => [
1356 'heading' => __('Upgrade to Pro Plus for multiple popups', 'reviews-feed'),
1357 'description' => __('Create unlimited notification popups and display different popups on different pages.', 'reviews-feed'),
1358 'image' => 'upsell-review-alert.png',
1359 'buttons' => [
1360 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-multiple-modal&utm_content=LiteUsers50OFF',
1361 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-multiple-modal&utm_content=Upgrade'
1362 ],
1363 'includeContent' => true
1364 ],
1365 'reviewAlertTargeting' => [
1366 'heading' => __('Upgrade to Pro for page targeting', 'reviews-feed'),
1367 'description' => __('Control exactly where your notification popups appear with specific page targeting and exclusions.', 'reviews-feed'),
1368 'image' => 'upsell-review-alert.png',
1369 'buttons' => [
1370 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-targeting-modal&utm_content=LiteUsers50OFF',
1371 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-targeting-modal&utm_content=Upgrade'
1372 ],
1373 'includeContent' => true
1374 ],
1375 'reviewAlertBranding' => [
1376 'heading' => __('Remove Smash Balloon Branding', 'reviews-feed'),
1377 'description' => __('Present a clean, professional look by removing the \'Powered by\' badge from your notification popups.', 'reviews-feed'),
1378 'image' => 'upsell-review-alert.png',
1379 'buttons' => [
1380 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-branding-modal&utm_content=LiteUsers50OFF',
1381 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-branding-modal&utm_content=Upgrade',
1382 'learnMore' => 'https://smashballoon.com/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-branding-modal&utm_content=LearnMore'
1383 ],
1384 'includeContent' => true
1385 ]
1386 ];
1387 }
1388
1389 /**
1390 * Get Pro Plus upsell content for Pro users
1391 *
1392 * Returns upsell modals for features that require Pro Plus tier.
1393 *
1394 * @since 2.5.0
1395 *
1396 * @return array
1397 */
1398 public static function get_pro_plus_upsell_content()
1399 {
1400 return [
1401 'reviewAlertMultiple' => [
1402 'heading' => __('Upgrade to Pro Plus for multiple popups', 'reviews-feed'),
1403 'description' => __('Create unlimited notification popups and display different popups on different pages.', 'reviews-feed'),
1404 'image' => 'upsell-review-alert.png',
1405 'buttons' => [
1406 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-pro&utm_source=review-alert&utm_medium=popup-multiple-modal&utm_content=UpgradeToProPlus'
1407 ],
1408 'includeContent' => false
1409 ],
1410 'reviewAlertTargeting' => [
1411 'heading' => __('Upgrade to Pro Plus for page targeting', 'reviews-feed'),
1412 'description' => __('Control exactly where your notification popups appear with specific page targeting and exclusions.', 'reviews-feed'),
1413 'image' => 'upsell-review-alert.png',
1414 'buttons' => [
1415 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-pro&utm_source=review-alert&utm_medium=popup-targeting-modal&utm_content=UpgradeToProPlus'
1416 ],
1417 'includeContent' => false
1418 ],
1419 'reviewAlertBranding' => [
1420 'heading' => __('Remove Smash Balloon Branding', 'reviews-feed'),
1421 'description' => __('Present a clean, professional look by removing the \'Powered by\' badge from your notification popups.', 'reviews-feed'),
1422 'image' => 'upsell-review-alert.png',
1423 'buttons' => [
1424 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-pro&utm_source=review-alert&utm_medium=popup-branding-modal&utm_content=UpgradeToProPlus',
1425 'learnMore' => 'https://smashballoon.com/reviews-feed/?utm_campaign=reviews-pro&utm_source=review-alert&utm_medium=popup-branding-modal&utm_content=LearnMore'
1426 ],
1427 'includeContent' => true
1428 ]
1429 ];
1430 }
1431
1432 /**
1433 * Get List of Upsell Sidebar Cards
1434 *
1435 * @since 1.1
1436 *
1437 * @return array
1438 */
1439 public static function sidebar_upsell_cards()
1440 {
1441 if (Util::sbr_is_pro()) {
1442 return [];
1443 }
1444 return [
1445 [
1446 'heading' => __('Display images', 'reviews-feed'),
1447 'description' => __('With the Pro version enable images like avatars and review photos', 'reviews-feed'),
1448 'image' => 'upswidget-images.png',
1449 'modal' => 'authorImageModal'
1450 ],
1451 [
1452 'heading' => __('Get carousel layout', 'reviews-feed'),
1453 'description' => __('Show your reviews in a neat carousel with Review Feed Pro', 'reviews-feed'),
1454 'image' => 'upswidget-carousel.png',
1455 'modal' => 'carouselModal'
1456 ],
1457 [
1458 'heading' => __('Change your feed style with one-click templates!', 'reviews-feed'),
1459 'description' => __('Over 12 templates to choose from', 'reviews-feed'),
1460 'image' => 'upswidget-templates.png',
1461 'modal' => 'templateModal'
1462 ],
1463 [
1464 'heading' => __('Only show selected reviews with Moderation mode', 'reviews-feed'),
1465 'description' => __('Hide or show only certain reviews', 'reviews-feed'),
1466 'image' => 'upswidget-moderation.png',
1467 'modal' => 'moderationModal'
1468 ]
1469 ];
1470 }
1471
1472
1473 /**
1474 * Coerce a review's `provider`, `reviewer` and `source` into the array shapes
1475 * every reader expects ($review['provider']['name'], $review['reviewer']['name'],
1476 * $review['source']['id'], …). The relay can emit these as scalars (e.g.
1477 * `provider` => 'google', or an error-shaped payload yielding a scalar
1478 * `reviewer`/`source`), and PHP 7-era caches stored them that way in
1479 * `json_data`. On PHP 8.0+ a direct nested read on a scalar fatals with
1480 * "Cannot access offset of type string on string".
1481 *
1482 * This is the single source of truth used by SinglePostCache (both
1483 * constructors) and every raw/DB-decoded read site: the dedup key build in
1484 * PostAggregator::remove_duplicated_posts_list (front-end render, Feed.php:172)
1485 * reads source['id'] + reviewer['name'] + provider['name'] off the raw post,
1486 * parse_single_review reads reviewer/provider, and
1487 * SBR_Feed_Saver_Manager::duplicate_collection reads reviewer/source.
1488 *
1489 * SMASH-1578 guarded whole-review + source shapes at the cache loops, SMASH-1587
1490 * added provider; this also covers a scalar reviewer/source that the
1491 * is_array($single_review)-only cache guard lets through to store()/dedup
1492 * (reachable on WPSA-63160's associative-keyed payload). Healthy array data is
1493 * left intact — missing keys are filled with empty strings (via array union),
1494 * so the dedup key degrades to an empty segment instead of crashing.
1495 *
1496 * @param mixed $review
1497 * @return mixed Untouched when not an array (callers' is_array guards handle that).
1498 */
1499 public static function normalize_review_shape($review)
1500 {
1501 if (!is_array($review)) {
1502 return $review;
1503 }
1504
1505 if (isset($review['provider']) && is_string($review['provider'])) {
1506 $review['provider'] = ['name' => $review['provider']];
1507 } elseif (!isset($review['provider']) || !is_array($review['provider'])) {
1508 $review['provider'] = ['name' => ''];
1509 }
1510 if (!isset($review['reviewer']) || !is_array($review['reviewer'])) {
1511 $review['reviewer'] = [];
1512 }
1513 if (!isset($review['source']) || !is_array($review['source'])) {
1514 $review['source'] = [];
1515 }
1516 // Image containers iterated by resize_images() + add_local_image_urls().
1517 // A scalar here would fatal the foreach on PHP 8. Coerce a present
1518 // non-array to [] (leave absent untouched — they're optional). Element
1519 // shape is guarded at the loop sites (a flat URL-string element).
1520 foreach (['media', 'reviews_photos', 'reviewer_photos'] as $image_key) {
1521 if (isset($review[$image_key]) && !is_array($review[$image_key])) {
1522 $review[$image_key] = [];
1523 }
1524 }
1525
1526 // Guarantee the exact keys every reader assumes are present and string,
1527 // so a scalar/partial provider, reviewer or source degrades to empty
1528 // segments instead of "Cannot access offset…" fatals or "Undefined array
1529 // key" notices in the dedup key build / parse_single_review / preview
1530 // backfill (SMASH-1587 + PR #484 Copilot review + WPSA-63160 follow-up).
1531 $review['provider'] += ['name' => ''];
1532 $review['reviewer'] += ['name' => '', 'avatar' => ''];
1533 $review['source'] += ['id' => '', 'url' => ''];
1534 if (!is_string($review['provider']['name'])) {
1535 $review['provider']['name'] = '';
1536 }
1537 if (!is_string($review['reviewer']['name'])) {
1538 $review['reviewer']['name'] = '';
1539 }
1540 if (!is_string($review['source']['id'])) {
1541 // Preserve a numeric id (cast), but never (string)-cast an array/object —
1542 // that would emit an "Array to string conversion" notice (PR #482 Copilot).
1543 $review['source']['id'] = is_scalar($review['source']['id'])
1544 ? (string) $review['source']['id']
1545 : '';
1546 }
1547
1548 return $review;
1549 }
1550
1551 /**
1552 * Transform Single Review for storing purposes
1553 *
1554 * @since 1.4
1555 *
1556 * @return array
1557 */
1558 public static function parse_single_review($review, $provider_id, $review_id)
1559 {
1560 // Provider can arrive as a scalar slug (SMASH-1587); normalize before the
1561 // $review['provider']['name'] read below so it can't fatal on PHP 8.
1562 $review = self::normalize_review_shape($review);
1563 $name = $review['reviewer']['name'];
1564 $name_array = explode(' ', $name);
1565 $first_name = isset($review['reviewer']['first_name']) ? $review['reviewer']['first_name'] : $name_array[0];
1566 $last_name = isset($review['reviewer']['last_name']) ? $review['reviewer']['last_name'] : (isset($name_array[1]) ? $name_array[1] : '');
1567
1568 $sanitized_review = [
1569 'time' => $review['time'],
1570 'rating' => $review['rating'],
1571 'provider_id' => $provider_id,
1572 'review_id' => $review_id,
1573 'text' => $review['text'],
1574 'title' => isset($review['title']) ? $review['title'] : substr($review['text'], 0, 40),
1575 'reviewer' => [
1576 'name' => $name,
1577 'first_name' => $first_name,
1578 'last_name' => $last_name,
1579 'avatar' => $review['reviewer']['avatar']
1580 ],
1581 'provider' => [
1582 'name' => $review['provider']['name']
1583 ],
1584 'source' => [
1585 'id' => $provider_id,
1586 'url' => ''
1587 ]
1588 ];
1589 return $sanitized_review;
1590 }
1591
1592
1593 public static function is_facebook_collection_post($post)
1594 {
1595 return ( isset($post['provider']) && isset($post['provider']['name']) && $post['provider']['name'] === 'facebook' && isset($post['provider_id']) && strpos($post['provider_id'], 'collection') !== false ) === true;
1596 }
1597
1598 /**
1599 * Convert Object to Array
1600 *
1601 * @return array
1602 *
1603 * @since 1.0
1604 */
1605 public static function object_to_array($data)
1606 {
1607 if (is_object($data)) {
1608 $data = json_decode(json_encode($data), true);
1609 }
1610 return $data;
1611 }
1612
1613 /**
1614 * Convert Object to Array
1615 *
1616 * @return array
1617 *
1618 * @since 1.0
1619 */
1620 public static function get_free_retriever_data()
1621 {
1622 if (Util::sbr_is_pro()) {
1623 $retriever = new \SmashBalloon\Reviews\Pro\Utils\FreeRetriever();
1624 } else {
1625 $retriever = new \SmashBalloon\Reviews\Common\Utils\FreeRetriever();
1626 }
1627 return $retriever->get_settings();
1628 }
1629
1630 /**
1631 * Get Feeds Settings
1632 *
1633 * @since 1.0
1634 *
1635 * @return string
1636 */
1637 public static function get_settings_page_errors()
1638 {
1639 $output = '## ERROR LOGS: ## </br></br>';
1640 $errors = SBR_Error_Handler::get_errors();
1641 $errors = array_reverse($errors);
1642 foreach ($errors as $error) {
1643 $output .= json_encode($error);
1644 $output .= '</br></br>';
1645 }
1646 $output .= '</br>';
1647
1648 return $output;
1649 }
1650
1651 /**
1652 * Get Local Images/Avatar
1653 * Upload folder name
1654 *
1655 * @since 2.0
1656 *
1657 * @return string
1658 */
1659 public static function get_upload_folder_name()
1660 {
1661 $upload = wp_upload_dir();
1662 $folder = trailingslashit($upload['basedir']) . trailingslashit('sbr-feed-images');
1663 return $folder;
1664 }
1665
1666 public static function should_store_local_images()
1667 {
1668 $settings = get_option('sbr_settings', sbr_plugin_settings_defaults());
1669 if (! is_array($settings)) {
1670 $settings = sbr_plugin_settings_defaults();
1671 }
1672 return !empty($settings['optimize_images']) ? $settings['optimize_images'] : true;
1673 }
1674
1675 /**
1676 * Convert an ISO-3166 alpha-2 country code to its regional-indicator flag emoji.
1677 *
1678 * Shared by the AliExpress feed author element (post-elements/author.php) and
1679 * the Review Alerts popup (review-alerts/popup.php) so both render the same
1680 * glyph from the same rules. Returns '' when the input is not a 2-letter code,
1681 * or when mbstring is unavailable (some shared hosts ship without it) — callers
1682 * simply skip the flag rather than fataling the render.
1683 *
1684 * @param string $cc Country code (e.g. "US", "de").
1685 * @return string The flag emoji, or '' if not derivable.
1686 */
1687 public static function country_flag_emoji($cc)
1688 {
1689 $cc = trim((string) $cc);
1690 // ISO-3166 alpha-2 only. ASCII regex, not ctype_alpha() — ctype_* is
1691 // locale-dependent and can accept non-ASCII letters, which would flow
1692 // into ord() and produce wrong codepoints (copilot #467).
1693 if (! preg_match('/^[A-Za-z]{2}$/', $cc) || ! function_exists('mb_chr')) {
1694 return '';
1695 }
1696 $up = strtoupper($cc);
1697 $offset = 0x1F1E6 - ord('A');
1698 return mb_chr(ord($up[0]) + $offset, 'UTF-8') . mb_chr(ord($up[1]) + $offset, 'UTF-8');
1699 }
1700
1701 }
1702