PluginProbe ʕ •ᴥ•ʔ
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More / trunk
Reviews Feed – Add Testimonials and Customer Reviews From Google Reviews, Yelp, TripAdvisor, and More vtrunk
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 6 days ago Customizer 6 days ago Exceptions 6 months ago Helpers 2 months ago Integrations 6 days ago Migrations 3 years ago ReviewAlerts 3 weeks ago Services 6 days ago Settings 6 days ago Support 3 months ago Traits 1 month ago UsageTracking 6 days ago Utils 2 months ago AuthorizationStatusCheck.php 1 month ago BusinessDataCache.php 6 months ago Clear_Cache.php 3 months ago Container.php 6 months ago DisplayElements.php 2 months ago Email_Notification.php 6 months ago Error_Reporter.php 3 months ago Feed.php 3 weeks ago FeedCache.php 5 months ago FeedCacheUpdater.php 3 months ago FeedDisplay.php 1 month ago Feed_Locator.php 6 months ago Parser.php 3 weeks ago PostAggregator.php 2 months ago RemoteRequest.php 6 days ago SBR_Education.php 3 months ago SBR_Schema_Service.php 3 weeks ago SBR_Settings.php 6 months ago ServiceContainer.php 6 days ago SinglePostCache.php 3 weeks ago TemplateRenderer.php 6 months ago Tooltip_Wizard.php 1 month ago Util.php 6 days ago
Util.php
1961 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 // Same shape as google and yelp above. The page itself is being
109 // rewritten for Terra under SMASH-1974 — until it is, it still walks
110 // the reader to the retired ?screen=credentials portal.
111 'docLink' => 'https://smashballoon.com/doc/creating-a-tripadvisor-api-key/?utm_campaign=' . $campaign . '&utm_source=settings&utm_medium=docs'
112 ],
113 [
114 'type' => 'yelp',
115 'name' => 'Yelp',
116 'heading' => __('Page URL', 'reviews-feed'),
117 'placeholder' => __('https://yelp.com/...', 'reviews-feed'),
118 'docLink' => 'https://smashballoon.com/doc/creating-a-yelp-api-key/?utm_campaign=' . $campaign . '&utm_source=settings&utm_medium=docs'
119 ],
120 [
121 'type' => 'trustpilot',
122 'name' => 'TrustPilot',
123 'heading' => __('Page URL', 'reviews-feed'),
124 'placeholder' => __('https://trustpilot.com/review/...', 'reviews-feed'),
125 'onlyDetails' => true
126 ],
127 [
128 'type' => 'wordpress.org',
129 'name' => 'WordPress.org',
130 'heading' => __('Page URL', 'reviews-feed'),
131 'placeholder' => __('https://wordpress.org/...', 'reviews-feed'),
132 'onlyDetails' => true
133 ],
134 [
135 'type' => 'woocommerce',
136 'name' => 'WooCommerce',
137 'heading' => __('Product', 'reviews-feed'),
138 'placeholder' => __('Select a product', 'reviews-feed'),
139 'onlyDetails' => true,
140 'isLocal' => true,
141 'pluginRequired' => ! ( class_exists('WooCommerce') || function_exists('WC') ),
142 'pluginRequiredMessage' => __('Enable WooCommerce plugin to use it as a source', 'reviews-feed'),
143 ],
144 array_merge(
145 [
146 'type' => 'edd',
147 'name' => 'Easy Digital Downloads',
148 'heading' => __('Download', 'reviews-feed'),
149 'placeholder' => __('Select a download', 'reviews-feed'),
150 'onlyDetails' => true,
151 'isLocal' => true,
152 ],
153 self::get_edd_provider_status()
154 ),
155 [
156 'type' => 'airbnb',
157 'name' => 'Airbnb',
158 'heading' => __('Listing URL', 'reviews-feed'),
159 'placeholder' => __('https://www.airbnb.com/rooms/...', 'reviews-feed'),
160 'onlyDetails' => true
161 ],
162 [
163 'type' => 'booking',
164 'name' => 'Booking.com',
165 'heading' => __('Hotel URL', 'reviews-feed'),
166 'placeholder' => __('https://www.booking.com/hotel/...', 'reviews-feed'),
167 'onlyDetails' => true
168 ],
169 [
170 'type' => 'aliexpress',
171 'name' => 'AliExpress',
172 'heading' => __('Product URL', 'reviews-feed'),
173 'placeholder' => __('https://www.aliexpress.com/item/...', 'reviews-feed'),
174 'onlyDetails' => true
175 ]
176 ];
177
178 // Filter out disabled providers
179 $providers = array_filter($providers, function ($provider) {
180 return ! self::is_provider_disabled($provider['type']);
181 });
182
183 return array_values($providers);
184 }
185
186 /**
187 * Used as a listener for the account connection process. If
188 * data is returned from the account connection processed it's used
189 * to generate the list of possible sources to chose from.
190 *
191 * @return array|bool
192 *
193 * @since 1.0
194 */
195 public static function maybe_source_connection_data()
196 {
197 $nonce = !empty($_GET['cff_con']) ? sanitize_key($_GET['cff_con']) : '';
198 if (!wp_verify_nonce($nonce, 'cff_con')) {
199 return false;
200 }
201
202 if (
203 isset($_GET['cff_access_token'])
204 && isset($_GET['cff_final_response'])
205 && $_GET['cff_final_response'] == 'true'
206 ) {
207 $access_token = sanitize_text_field(wp_unslash($_GET['cff_access_token']));
208 $return = Util::retrieve_available_pages($access_token);
209
210 if ($return) {
211 return $return;
212 } else {
213 return array('error' => __('Unable to connect to Facebook to retrieve account information.', 'reviews-feed'));
214 }
215 }
216 return false;
217 }
218
219 /**
220 * Uses the Facebook API to retrieve a list of pages for the
221 * access token
222 *
223 * @param string $access_token
224 *
225 * @return array|bool
226 *
227 * @since 1.0
228 */
229 public static function retrieve_available_pages($access_token)
230 {
231 //Get User Info
232 $user_url = 'https://graph.facebook.com/me?fields=name,id,picture&access_token=' . urlencode($access_token);
233 $user_id_data = wp_remote_retrieve_body(wp_remote_get($user_url));
234 $user_id_data_arr = json_decode($user_id_data, true);
235
236 $url = 'https://graph.facebook.com/me/accounts?fields=access_token,name,id,overall_star_rating,rating_count&limit=500&access_token=' . urlencode($access_token);
237 $pages_data = wp_remote_retrieve_body(wp_remote_get($url));
238 $pages_data_arr = json_decode($pages_data, true);
239
240
241 if (isset($pages_data_arr['data'])) {
242 $return = [
243 'user' => $user_id_data_arr,
244 'pages' => $pages_data_arr['data'],
245 ];
246
247 return $return;
248 } elseif (isset($pages_data_arr['error'])) {
249 return $pages_data_arr;
250 } else {
251 return [
252 'error' => [
253 'code' => 'HTTP Request',
254 'message' => __('Your server could not complete a remote request to Facebook\'s API. Your host may be blocking access or
255 there may be a problem with your server.', 'reviews-feed')
256 ]
257 ];
258 }
259 }
260
261 public static function currentPageIs($page)
262 {
263 $current_screen = get_current_screen();
264 return $current_screen !== null && !empty($current_screen) && strpos($current_screen->id, $page) !== false;
265 }
266
267 /**
268 * Check Notices for no Provider API KEY
269 *
270 *
271 * @return boolean
272 *
273 * @since 1.0
274 */
275 public static function check_notice_provider_nokey($api_limits)
276 {
277 $noapi_providers = ['wordpress.org', 'trustpilot'];
278 $is_true = $api_limits == $noapi_providers ? false : true;
279
280 foreach ($noapi_providers as $provider) {
281 if ($api_limits == [$provider]) {
282 $is_true = false;
283 }
284 }
285 return $is_true;
286 }
287
288 /**
289 * Returns a list of notices to be displayed in the SB Reviews Pages
290 *
291 *
292 * @return array
293 *
294 * @since 1.0
295 */
296 public static function get_plugin_notices()
297 {
298 $notices = [];
299
300 $api_limits = get_option('sbr_apikeys_limit', []);
301 $should_show_notice = is_array($api_limits) && sizeof($api_limits) > 0 && Util::check_notice_provider_nokey($api_limits);
302 if ($should_show_notice) {
303 array_push($notices, [
304 'type' => 'error',
305 'heading' => __('New reviews will not display until an API key is entered for your sources.', 'reviews-feed'),
306 'description' => __('Due to API limitations your feeds will not show new reviews until you enter an API key on the settings page.', 'reviews-feed'),
307 'actions' => [
308 [
309 'type' => 'primary',
310 'text' => __('How to create an API key', 'reviews-feed'),
311 'link' => admin_url('admin.php?page=sbr-settings')
312 ],
313 [
314 'type' => 'secondary',
315 'text' => __('Go to Settings page', 'reviews-feed'),
316 'link' => admin_url('admin.php?page=sbr-settings')
317 ]
318 ]
319 ]);
320 }
321
322 $noAPIKeyProviders = ['trustpilot', 'wordpress.org'];
323 $noAPIProviderxists = count(array_intersect($noAPIKeyProviders, $api_limits)) ? true : false;
324
325 if ($noAPIProviderxists) {
326 array_push($notices, [
327 'type' => 'error',
328 'heading' => __('You have reached the number of allowed sources.', 'reviews-feed'),
329 'description' => __('Due to API limitations your feeds will not show new reviews because you have reached the number of allowed sources.', 'reviews-feed'),
330 ]);
331 }
332
333 // Unshift, not push. The customizer renders pluginNotices[0] and nothing
334 // else — PluginNotices.js declares setCurrentNoticeIndex and never calls
335 // it — so a third notice is invisible whenever an earlier one fires. Both
336 // notices above gate on `sbr_apikeys_limit`, which is currently never
337 // populated (SMASH-1901 D3), so appending happened to work; the moment
338 // that bug is fixed it would have silently suppressed this one on exactly
339 // the sites hitting source limits. This has a hard external deadline and
340 // they do not, so it goes first rather than depending on a bug staying
341 // broken.
342 $tripadvisor_sunset = self::get_tripadvisor_sunset_notice();
343 if ($tripadvisor_sunset !== null) {
344 array_unshift($notices, $tripadvisor_sunset);
345 }
346
347 return $notices;
348 }
349
350 /**
351 * Timestamp TripAdvisor deprecates every legacy Content API key.
352 *
353 * Verbatim from docs.terra.tripadvisor.com/docs/faq.md: "The legacy Content
354 * API will be sunset and API keys deprecated on August 31, 2026."
355 */
356 const TRIPADVISOR_CONTENT_API_SUNSET = '2026-08-31T23:59:59+00:00';
357
358 /**
359 * Warn only the sites that have to act (SMASH-1835).
360 *
361 * A site running its OWN legacy TripAdvisor key loses it on the sunset date,
362 * and only the site owner can replace it. A keyless site needs no notice: it
363 * rides the relay's shared credential and moves platforms without any action
364 * here, so nagging it would be noise about something it cannot fix.
365 *
366 * @return array|null Notice in the get_plugin_notices() shape, or null.
367 */
368 public static function get_tripadvisor_sunset_notice()
369 {
370 $stored_keys = get_option('sbr_apikeys', []);
371 $key = is_array($stored_keys) && isset($stored_keys['tripadvisor']) && is_string($stored_keys['tripadvisor'])
372 ? trim($stored_keys['tripadvisor'])
373 : '';
374
375 // No key of their own, or already a Terra key — nothing to do.
376 if ($key === '' || self::is_tripadvisor_terra_key($key)) {
377 return null;
378 }
379
380 // A dying key only matters if something is actually using it.
381 if (empty(SBR_Sources::sources_by_providers(['tripadvisor']))) {
382 return null;
383 }
384
385 $sunset = strtotime(self::TRIPADVISOR_CONTENT_API_SUNSET);
386 $has_passed = $sunset !== false && time() > $sunset;
387
388 // The const is the only place the date lives. It used to be hardcoded in
389 // the copy as well, so moving the const would have left the heading
390 // quoting the old date while the branch flipped. Interpolating also keeps
391 // a date change from invalidating every translation of these strings, and
392 // renders in the site's own format.
393 // get_option() is mixed, and a site can store an empty date_format.
394 // 'F j, Y' is WP's own default and renders the date the copy used to
395 // hardcode.
396 $date_format = get_option('date_format');
397 $date_format = is_string($date_format) && $date_format !== '' ? $date_format : 'F j, Y';
398
399 $sunset_date = $sunset === false
400 ? substr(self::TRIPADVISOR_CONTENT_API_SUNSET, 0, 10)
401 : date_i18n($date_format, $sunset);
402
403 $heading = $has_passed
404 ? __('Your TripAdvisor API key has stopped working', 'reviews-feed')
405 /* translators: %s: date TripAdvisor retires the legacy Content API. */
406 : sprintf(__('Your TripAdvisor API key stops working on %s', 'reviews-feed'), $sunset_date);
407
408 // Two ways out, and the cheaper one is listed first on purpose: removing
409 // the key falls back to the connection built into the plugin and needs
410 // no account anywhere. No doc link here on purpose: the notice's job is
411 // the two remedies, and the key doc is one click away on the page the
412 // button lands on.
413 $description = $has_passed
414 /* translators: %s: date TripAdvisor retired the legacy Content API. */
415 ? sprintf(__('TripAdvisor retired the API this key belongs to on %s. Remove the key on the Settings page to switch back to the connection built into the plugin, or replace it with a key from TripAdvisor\'s new Terra platform.', 'reviews-feed'), $sunset_date)
416 : __('TripAdvisor is retiring the API this key belongs to. Before then, either remove the key on the Settings page to switch back to the connection built into the plugin, or replace it with a key from TripAdvisor\'s new Terra platform.', 'reviews-feed');
417
418 return [
419 // 'error' for both states, not 'warning': the renderer puts this
420 // straight onto data-type (PluginNotices.js:25) and sb-common.css
421 // only styles [data-type=error], so anything else renders unstyled.
422 'type' => 'error',
423 'heading' => $heading,
424 'description' => $description,
425 'actions' => [
426 [
427 'type' => 'primary',
428 'text' => __('Manage API Keys', 'reviews-feed'),
429 // Anchored: the API keys section sits ~1700px down a ~3200px page, so
430 // landing at the top left the remedy off screen. The customizer scrolls
431 // to this id once the section renders (SettingsPage.js).
432 'link' => admin_url('admin.php?page=sbr-settings') . '#sbr-settings-apikeys',
433 ],
434 ],
435 ];
436 }
437
438 /**
439 * Terra issues UUIDs; legacy Content API keys are 32 hex characters. The two
440 * sets cannot overlap, which is what lets the relay route per key — mirror of
441 * TripAdvisor::looksLikeTerraKey() in sb-relay.
442 *
443 * @param string $key
444 *
445 * @return bool
446 */
447 public static function is_tripadvisor_terra_key($key)
448 {
449 if (!is_string($key)) {
450 return false;
451 }
452
453 return preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $key) === 1;
454 }
455
456 /**
457 * Get Smahballoon Plugins Info
458 *
459 * @since 1.0
460 */
461 public static function get_plugins_info()
462 {
463 $installed_plugins = get_plugins();
464 $campaign = self::sbr_is_pro() ? 'reviews-pro' : 'reviews-free';
465
466 $plugins_list = [
467 'facebook' => [
468 'free' => 'custom-facebook-feed/custom-facebook-feed.php',
469 'pro' => 'custom-facebook-feed-pro/custom-facebook-feed.php',
470 'link' => 'https://smashballoon.com/custom-facebook-feed/?utm_campaign=' . $campaign . '&utm_source=about-us&utm_medium=marketing'
471 ],
472 'instagram' => [
473 'free' => 'instagram-feed/instagram-feed.php',
474 'pro' => 'instagram-feed-pro/instagram-feed.php',
475 'link' => 'https://smashballoon.com/instagram-feed/?utm_campaign=' . $campaign . '&utm_source=about-us&utm_medium=marketing'
476 ],
477 'twitter' => [
478 'free' => 'custom-twitter-feeds/custom-twitter-feed.php',
479 'pro' => 'custom-twitter-feeds-pro/custom-twitter-feed.php',
480 'link' => 'https://smashballoon.com/custom-twitter-feeds/?utm_campaign=' . $campaign . '&utm_source=about-us&utm_medium=marketing'
481 ],
482 'youtube' => [
483 'free' => 'feeds-for-youtube/youtube-feed.php',
484 'pro' => 'youtube-feed-pro/youtube-feed.php',
485 'link' => 'https://smashballoon.com/youtube-feed/?utm_campaign=' . $campaign . '&utm_source=about-us&utm_medium=marketing'
486 ]
487 ];
488
489 foreach ($plugins_list as $name => $plugin) {
490 $type = 'none';
491 $activated = 'none';
492 if (isset($installed_plugins[$plugin['free']])) {
493 $type = 'free';
494 $activated = is_plugin_active($plugin['free']);
495 }
496 if (isset($installed_plugins[$plugin['pro']])) {
497 $type = 'pro';
498 $activated = is_plugin_active($plugin['pro']);
499 }
500 $plugins_list[$name]['activated'] = $activated;
501 $plugins_list[$name]['type'] = $type;
502 }
503
504 return [
505 'facebook' => [
506 'plugin' => $plugins_list['facebook']['pro'],
507 'link' => $plugins_list['facebook']['link'],
508 'download_plugin' => 'https://downloads.wordpress.org/plugin/custom-facebook-feed.zip',
509 'title' => __('Custom Facebook Feed', 'reviews-feed'),
510 'description' => __('Add Facebook posts from your timeline, albums and much more.', 'reviews-feed'),
511 'icon' => 'fb-icon.svg',
512 'activated' => $plugins_list['facebook']['activated'],
513 'type' => $plugins_list['facebook']['type'],
514 ],
515 'instagram' => [
516 'plugin' => $plugins_list['instagram']['pro'],
517 'link' => $plugins_list['instagram']['link'],
518 'download_plugin' => 'https://downloads.wordpress.org/plugin/instagram-feed.zip',
519 'title' => __('Instagram Feed', 'reviews-feed'),
520 'description' => __('A quick and elegant way to add your Instagram posts to your website. ', 'reviews-feed'),
521 'icon' => 'insta-icon.svg',
522 'activated' => $plugins_list['instagram']['activated'],
523 'type' => $plugins_list['instagram']['type'],
524 ],
525 'twitter' => [
526 'plugin' => $plugins_list['twitter']['pro'],
527 'link' => $plugins_list['twitter']['link'],
528 'download_plugin' => 'https://downloads.wordpress.org/plugin/custom-twitter-feeds.zip',
529 'title' => __('Custom Twitter Feeds', 'reviews-feed'),
530 'description' => __('A customizable way to display tweets from your Twitter account. ', 'reviews-feed'),
531 'icon' => 'twitter-icon.svg',
532 'activated' => $plugins_list['twitter']['activated'],
533 'type' => $plugins_list['twitter']['type'],
534 ],
535 'youtube' => [
536 'plugin' => $plugins_list['youtube']['pro'],
537 'link' => $plugins_list['youtube']['link'],
538 'download_plugin' => 'https://downloads.wordpress.org/plugin/feeds-for-youtube.zip',
539 'title' => __('Feeds for YouTube', 'reviews-feed'),
540 'description' => __('A simple yet powerful way to display videos from YouTube. ', 'reviews-feed'),
541 'icon' => 'youtube-icon.svg',
542 'activated' => $plugins_list['youtube']['activated'],
543 'type' => $plugins_list['youtube']['type'],
544 ]
545 ];
546 }
547
548 /**
549 * Get Smahballoon Recommended Plugins Info
550 *
551 * @since 1.0
552 */
553 public static function get_smashballoon_recommended_plugins_info()
554 {
555 $installed_plugins = get_plugins();
556 return [
557 'wpforms' => [
558 'plugin' => 'wpforms-lite/wpforms.php',
559 'download_plugin' => 'https://downloads.wordpress.org/plugin/wpforms-lite.zip',
560 'title' => __('WPForms', 'reviews-feed'),
561 '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'),
562 'icon' => 'plugin-wpforms.png',
563 'installed' => isset($installed_plugins['wpforms-lite/wpforms.php']),
564 'activated' => is_plugin_active('wpforms-lite/wpforms.php'),
565 ],
566 'monsterinsights' => [
567 'plugin' => 'google-analytics-for-wordpress/googleanalytics.php',
568 'download_plugin' => 'https://downloads.wordpress.org/plugin/google-analytics-for-wordpress.zip',
569 'title' => __('MonsterInsights', 'reviews-feed'),
570 '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'),
571 'icon' => 'plugin-mi.png',
572 'installed' => isset($installed_plugins['google-analytics-for-wordpress/googleanalytics.php']),
573 'activated' => is_plugin_active('google-analytics-for-wordpress/googleanalytics.php'),
574 ],
575 'optinmonster' => [
576 'plugin' => 'optinmonster/optin-monster-wp-api.php',
577 'download_plugin' => 'https://downloads.wordpress.org/plugin/optinmonster.zip',
578 'title' => __('OptinMonster', 'reviews-feed'),
579 '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'),
580 'icon' => 'plugin-om.png',
581 'installed' => isset($installed_plugins['optinmonster/optin-monster-wp-api.php']),
582 'activated' => is_plugin_active('optinmonster/optin-monster-wp-api.php'),
583 ],
584 'wp_mail_smtp' => [
585 'plugin' => 'wp-mail-smtp/wp_mail_smtp.php',
586 'download_plugin' => 'https://downloads.wordpress.org/plugin/wp-mail-smtp.zip',
587 'title' => __('WP Mail SMTP', 'reviews-feed'),
588 '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'),
589 'icon' => 'plugin-smtp.png',
590 'installed' => isset($installed_plugins['wp-mail-smtp/wp_mail_smtp.php']),
591 'activated' => is_plugin_active('wp-mail-smtp/wp_mail_smtp.php'),
592 ],
593 'rafflepress' => [
594 'plugin' => 'rafflepress/rafflepress.php',
595 'download_plugin' => 'https://downloads.wordpress.org/plugin/rafflepress.zip',
596 'title' => __('RafflePress', 'reviews-feed'),
597 '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'),
598 'icon' => 'plugin-rp.png',
599 'installed' => isset($installed_plugins['rafflepress/rafflepress.php']),
600 'activated' => is_plugin_active('rafflepress/rafflepress.php'),
601 ],
602 'aioseo' => [
603 'plugin' => 'all-in-one-seo-pack/all_in_one_seo_pack.php',
604 'download_plugin' => 'https://downloads.wordpress.org/plugin/all-in-one-seo-pack.zip',
605 'title' => __('All in One SEO Pack', 'reviews-feed'),
606 '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'),
607 'icon' => 'plugin-seo.png',
608 'installed' => isset($installed_plugins['all-in-one-seo-pack/all_in_one_seo_pack.php']),
609 'activated' => is_plugin_active('all-in-one-seo-pack/all_in_one_seo_pack.php'),
610 ]
611 ];
612 }
613 /**
614 * Get other active plugins of Smash Balloon
615 *
616 * @since 4.4.0
617 */
618 public static function get_sb_active_plugins_info()
619 {
620 // get the WordPress's core list of installed plugins
621 if (!function_exists('get_plugins')) {
622 require_once ABSPATH . 'wp-admin/includes/plugin.php';
623 }
624 $installed_plugins = get_plugins();
625
626 $is_facebook_installed = false;
627 $facebook_plugin = 'custom-facebook-feed/custom-facebook-feed.php';
628 if (isset($installed_plugins['custom-facebook-feed-pro/custom-facebook-feed.php'])) {
629 $is_facebook_installed = true;
630 $facebook_plugin = 'custom-facebook-feed-pro/custom-facebook-feed.php';
631 } elseif (isset($installed_plugins['custom-facebook-feed/custom-facebook-feed.php'])) {
632 $is_facebook_installed = true;
633 }
634
635 $is_instagram_installed = false;
636 $instagram_plugin = 'instagram-feed/instagram-feed.php';
637 if (isset($installed_plugins['instagram-feed-pro/instagram-feed.php'])) {
638 $is_instagram_installed = true;
639 $instagram_plugin = 'instagram-feed-pro/instagram-feed.php';
640 } elseif (isset($installed_plugins['instagram-feed/instagram-feed.php'])) {
641 $is_instagram_installed = true;
642 }
643
644 $is_twitter_installed = false;
645 $twitter_plugin = 'custom-twitter-feeds/custom-twitter-feed.php';
646 if (isset($installed_plugins['custom-twitter-feeds-pro/custom-twitter-feed.php'])) {
647 $is_twitter_installed = true;
648 $twitter_plugin = 'custom-twitter-feeds-pro/custom-twitter-feed.php';
649 } elseif (isset($installed_plugins['custom-twitter-feeds/custom-twitter-feed.php'])) {
650 $is_twitter_installed = true;
651 }
652
653 $is_youtube_installed = false;
654 $youtube_plugin = 'feeds-for-youtube/youtube-feed.php';
655 if (isset($installed_plugins['youtube-feed-pro/youtube-feed-pro.php'])) {
656 $is_youtube_installed = true;
657 $youtube_plugin = 'youtube-feed-pro/youtube-feed-pro.php';
658 } elseif (isset($installed_plugins['feeds-for-youtube/youtube-feed.php'])) {
659 $is_youtube_installed = true;
660 }
661
662 $is_social_wall_installed = isset($installed_plugins['social-wall/social-wall.php']) ? true : false;
663 $social_wall_plugin = 'social-wall/social-wall.php';
664
665
666 return array(
667 'is_facebook_installed' => $is_facebook_installed,
668 'is_instagram_installed' => $is_instagram_installed,
669 'is_twitter_installed' => $is_twitter_installed,
670 'is_youtube_installed' => $is_youtube_installed,
671 'is_social_wall_installed' => $is_social_wall_installed,
672 'facebook_plugin' => $facebook_plugin,
673 'instagram_plugin' => $instagram_plugin,
674 'twitter_plugin' => $twitter_plugin,
675 'youtube_plugin' => $youtube_plugin,
676 'social_wall_plugin' => $social_wall_plugin,
677 'installed_plugins' => $installed_plugins
678 );
679 }
680
681 /**
682 * SBR Get Whitespace
683 *
684 * @since 1.0
685 *
686 * @param int $times
687 *
688 * @return string
689 */
690 public static function get_whitespace($times)
691 {
692 return str_repeat('&nbsp;', $times);
693 }
694
695 /**
696 * Get Site and Server Info
697 *
698 * @since 1.0
699 *
700 * @return string
701 */
702 public static function get_site_n_server_info()
703 {
704 $allow_url_fopen = ini_get('allow_url_fopen') ? "Yes" : "No";
705 $php_curl = is_callable('curl_init') ? "Yes" : "No";
706 $php_json_decode = function_exists("json_decode") ? "Yes" : "No";
707 $php_ssl = in_array('https', stream_get_wrappers()) ? "Yes" : "No";
708
709 $output = '## SITE/SERVER INFO: ##' . "</br>";
710 $output .= 'Plugin Version:' . self::get_whitespace(11) . SBR_MENU_SLUG . "</br>";
711 $output .= 'Site URL:' . self::get_whitespace(17) . site_url() . "</br>";
712 $output .= 'Home URL:' . self::get_whitespace(17) . home_url() . "</br>";
713 $output .= 'WordPress Version:' . self::get_whitespace(8) . get_bloginfo('version') . "</br>";
714 $output .= 'PHP Version:' . self::get_whitespace(14) . PHP_VERSION . "</br>";
715 $output .= 'Web Server Info:' . self::get_whitespace(10) . esc_html($_SERVER['SERVER_SOFTWARE']) . "</br>";
716 $output .= 'PHP allow_url_fopen:' . self::get_whitespace(6) . $allow_url_fopen . "</br>";
717 $output .= 'PHP cURL:' . self::get_whitespace(17) . $php_curl . "</br>";
718 $output .= 'JSON:' . self::get_whitespace(21) . $php_json_decode . "</br>";
719 $output .= 'SSL Stream:' . self::get_whitespace(15) . $php_ssl . "</br>";
720 $output .= "</br>";
721
722 return $output;
723 }
724
725 /**
726 * Get Active Plugins
727 *
728 * @since 1.0
729 *
730 * @return string
731 */
732 public static function get_active_plugins_info()
733 {
734 $plugins = get_plugins();
735 $active_plugins = get_option('active_plugins');
736 $output = "## ACTIVE PLUGINS: ## </br>";
737
738 foreach ($plugins as $plugin_path => $plugin) {
739 if (in_array($plugin_path, $active_plugins)) {
740 $output .= $plugin['Name'] . ': ' . $plugin['Version'] . "</br>";
741 }
742 }
743
744 $output .= "</br>";
745
746 return $output;
747 }
748
749
750 /**
751 * Get Global Settings
752 *
753 * @since 1.0
754 *
755 * @return string
756 */
757 public static function get_global_settings_info()
758 {
759 $output = '## GLOBAL SETTINGS: ## </br>';
760 $sbr_settings = get_option('sbr_settings', array());
761 if (! is_array($sbr_settings)) {
762 $sbr_settings = array();
763 }
764
765 $plugin_status = new AuthorizationStatusCheck();
766
767 $statuses = $plugin_status->get_statuses();
768
769 if (Util::sbr_is_pro()) {
770 $output .= 'License key: ';
771 if (isset($sbr_settings['license_key'])) {
772 $output .= esc_html($sbr_settings['license_key']);
773 } else {
774 $output .= ' Not added';
775 }
776
777 $output .= '</br>';
778 $output .= 'License Tier: ';
779 if (isset($statuses['license_tier'])) {
780 $output .= esc_html($statuses['license_tier']);
781 } else {
782 $output .= ' Not Set';
783 }
784 $output .= '</br>';
785 $output .= 'License status: ';
786 if (isset($sbr_settings['license_status'])) {
787 $output .= $sbr_settings['license_status'];
788 } else {
789 $output .= ' Inactive';
790 }
791 $output .= '</br>';
792 }
793 $output .= 'Preserve settings if plugin is removed: ';
794 $output .= isset($sbr_settings['preserve_settings']) && ($sbr_settings['preserve_settings']) ? 'Yes' : 'No';
795 $output .= '</br>';
796 $output .= 'Caching: ';
797 $output .= $statuses['license_tier'] === 3 ? 'Twice daily' : 'daily';
798
799 $output .= '</br>';
800 $output .= 'GDPR: ';
801 $output .= isset($sbr_settings['gdpr']) ? $sbr_settings['gdpr'] : ' Not setup';
802 $output .= '</br>';
803 $output .= 'Optimize Images: ';
804 $output .= isset($sbr_settings['optimize_images']) && $sbr_settings['optimize_images'] === true ? 'Enabled' : 'Disabled';
805 $output .= '</br>';
806 $output .= 'Usage Tracking: ';
807 $output .= \SmashBalloon\Reviews\Common\UsageTracking\Config::is_enabled() ? 'Enabled' : 'Disabled';
808 $output .= '</br>';
809 $output .= self::get_usage_tracking_debug_info();
810 $output .= 'Enqueue in Head: ';
811 $output .= isset($sbr_settings['enqueue_js_in_header']) && $sbr_settings['enqueue_js_in_header'] === true ? 'Enabled' : 'Disabled';
812 $output .= '</br>';
813 $output .= 'Admin Error Notice: ';
814 $output .= isset($sbr_settings['admin_error_notices']) && $sbr_settings['admin_error_notices'] === true ? 'Enabled' : 'Disabled';
815 $output .= '</br>';
816 $output .= 'Feed Issue Email Reports: ';
817 $output .= isset($sbr_settings['feed_issue_reports']) && $sbr_settings['feed_issue_reports'] === true ? 'Enabled' : 'Disabled';
818 $output .= '</br>';
819 $output .= '</br>';
820 return $output;
821 }
822
823 /**
824 * Usage-tracking send-state lines for the System Info page, so support
825 * can tell "never registered" from "registered but sends fail" from
826 * "cron not firing" at a glance.
827 *
828 * @return string
829 */
830 private static function get_usage_tracking_debug_info()
831 {
832 $state = get_option(\SmashBalloon\Reviews\Common\UsageTracking\Config::OPTION_TRACKING, array());
833 $state = is_array($state) ? $state : array();
834 $next = wp_next_scheduled(\SmashBalloon\Reviews\Common\UsageTracking\Config::CRON_HOOK);
835 $token = get_option(\SmashBalloon\Reviews\Common\UsageTracking\Config::OPTION_SITE_TOKEN, '');
836
837 $output = 'Usage Tracking Registered: ';
838 $output .= (is_string($token) && '' !== $token) ? 'Yes' : 'No';
839 $output .= '</br>';
840 $output .= 'Usage Tracking Last Sent: ';
841 $output .= ! empty($state['last_send']) ? gmdate('Y-m-d H:i:s', (int) $state['last_send']) . ' UTC' : 'Never';
842 $output .= '</br>';
843 $output .= 'Usage Tracking Last Attempt: ';
844 $output .= ! empty($state['last_attempt'])
845 ? gmdate('Y-m-d H:i:s', (int) $state['last_attempt']) . ' UTC (status ' . (isset($state['last_status']) ? (int) $state['last_status'] : 0) . ', ' . (isset($state['consecutive_failures']) ? (int) $state['consecutive_failures'] : 0) . ' consecutive failures)'
846 : 'Never';
847 $output .= '</br>';
848 $output .= 'Usage Tracking Next Scheduled: ';
849 $output .= $next ? gmdate('Y-m-d H:i:s', (int) $next) . ' UTC' : 'Not scheduled';
850 $output .= '</br>';
851
852 return $output;
853 }
854
855 /**
856 * Get Feeds Settings
857 *
858 * @since 1.0
859 *
860 * @return string
861 */
862 public static function get_sources_settings_info()
863 {
864 $output = '## SOURCES: ## </br>';
865 $source_list = SBR_Sources::get_sources_list();
866 foreach ($source_list as $feed) {
867 $output .= $feed['name'] . ' ( ' . strtoupper($feed['provider']) . ' => ' . $feed['account_id'] . ' )';
868 $output .= '</br>';
869 }
870 $output .= '</br>';
871
872 return $output;
873 }
874
875 /**
876 * Get Feeds Settings
877 *
878 * @since 1.0
879 *
880 * @return string
881 */
882 public static function get_api_settings_info()
883 {
884 $output = '## API KEYS: ## </br>';
885 $api_keys = get_option('sbr_apikeys', []);
886
887 foreach ($api_keys as $id => $api) {
888 $output .= ucfirst($id) . ' => ' . $api;
889 $output .= '</br>';
890 }
891 $output .= '</br>';
892
893 return $output;
894 }
895
896
897
898 /**
899 * Get Feeds Settings
900 *
901 * @since 1.0
902 *
903 * @return string
904 */
905 public static function get_feeds_settings_info()
906 {
907 $output = '## FEEDS: ## </br>';
908
909 $feeds_list = DB::get_feeds_list();
910
911 $i = 0;
912 foreach ($feeds_list as $feed) {
913 if ($i >= 25) {
914 break;
915 }
916 $output .= $feed['feed_name'];
917 if (isset($feed['settings'])) {
918 $output .= '</br>';
919 if (!empty($feed['sourcesList'])) {
920 foreach ($feed['sourcesList'] as $id => $source) {
921 $output .= esc_html($source['name']);
922 $output .= ' (' . esc_html(ucfirst($source['name'])) . ' => ' . esc_html($source['account_id']) . ')';
923 $output .= '</br>';
924 }
925 }
926 }
927 $output .= '</br>';
928 if (isset($feed['location_summary']) && count($feed['location_summary']) > 0) {
929 $first_feed = $feed['location_summary'][0];
930 if (!empty($first_feed['link'])) {
931 $output .= esc_html($first_feed['link']) . '?sb_debug';
932 $output .= '</br>';
933 }
934 }
935
936 if ($i < (count($feeds_list) - 1)) {
937 $output .= '</br>';
938 }
939 $i++;
940 }
941 $output .= '</br>';
942
943 return $output;
944 }
945
946 /**
947 * Get Posts Table Info
948 *
949 * @since 1.0
950 *
951 * @return string
952 */
953 public static function get_posts_table_info()
954 {
955 $output = '## POSTS: ## </br>';
956
957 global $wpdb;
958 $table_name = $wpdb->prefix . 'sbr_posts';
959 $feeds_posts_table_name = $wpdb->prefix . 'sbr_reviews_posts';
960
961 if ($wpdb->get_var("show tables like '$feeds_posts_table_name'") !== $feeds_posts_table_name) {
962 $output .= 'no feeds posts table</br>';
963 } else {
964 $last_result = $wpdb->get_results("SELECT * FROM $feeds_posts_table_name ORDER BY id DESC LIMIT 1;");
965 if (is_array($last_result) && isset($last_result[0])) {
966 $output .= '## FEEDS POSTS TABLE ##</br>';
967 foreach ($last_result as $column) {
968 foreach ($column as $key => $value) {
969 $output .= esc_html($key) . ': ' . esc_html($value) . '</br>';
970 }
971 }
972 } else {
973 $output .= 'feeds posts has no rows';
974 $output .= '</br>';
975 }
976 }
977 $output .= '</br>';
978 if ($wpdb->get_var("show tables like '$table_name'") !== $table_name) {
979 $output .= 'no posts table</br>';
980 } else {
981 $last_result = $wpdb->get_results("SELECT * FROM $table_name ORDER BY id DESC LIMIT 1;");
982 if (is_array($last_result) && isset($last_result[0])) {
983 $output .= '## POSTS TABLE ##';
984 $output .= '</br>';
985 foreach ($last_result as $column) {
986 foreach ($column as $key => $value) {
987 $output .= esc_html($key) . ': ' . esc_html($value) . '</br>';
988 }
989 }
990 } else {
991 $output .= 'posts has no rows</br>';
992 }
993 }
994 $output .= '</br>';
995
996 return $output;
997 }
998
999 /**
1000 * List of possible languages/translations
1001 *
1002 * @since 1.0
1003 *
1004 * @return array
1005 */
1006 public static function get_translation_languages($include_default = false)
1007 {
1008 $languages = [
1009 'default' => 'Default',
1010 '' => 'No Translation',
1011 'en' => 'English',
1012 'af' => 'Afrikaans',
1013 'am' => 'Amharic',
1014 'ar' => 'Arabic',
1015 'az' => 'Azerbaijani',
1016 'be' => 'Belarusian',
1017 'bg' => 'Bulgarian',
1018 'bn' => 'Bengali',
1019 'bs' => 'Bosnian',
1020 'ca' => 'Catalan',
1021 'cs' => 'Czech',
1022 'da' => 'Danish',
1023 'de' => 'German',
1024 'el' => 'Greek',
1025 'en-AU' => 'English (Australian)',
1026 'en-GB' => 'English (Great Britain)',
1027 'es' => 'Spanish',
1028 'es-419' => 'Spanish (Latin America)',
1029 'et' => 'Estonian',
1030 'eu' => 'Basque',
1031 'fa' => 'Farsi',
1032 'fi' => 'Finnish',
1033 'fil' => 'Filipino',
1034 'fr' => 'French',
1035 'fr-CA' => 'French (Canada)',
1036 'gl' => 'Galician',
1037 'gu' => 'Gujarati',
1038 'hi' => 'Hindi',
1039 'hr' => 'Croatian',
1040 'hu' => 'Hungarian',
1041 'hy' => 'Armenian',
1042 'id' => 'Indonesian',
1043 'is' => 'Icelandic',
1044 'it' => 'Italian ',
1045 'iw' => 'Hebrew',
1046 'ja' => 'Japanese',
1047 'ka' => 'Georgian',
1048 'kk' => 'Kazakh',
1049 'km' => 'Khmer',
1050 'kn' => 'Kannada',
1051 'ko' => 'Korean',
1052 'ky' => 'Kyrgyz',
1053 'lo' => 'Lao',
1054 'lt' => 'Lithuanian',
1055 'lv' => 'Latvian',
1056 'mk' => 'Macedonian',
1057 'ml' => 'Malayalam',
1058 'mn' => 'Mongolian',
1059 'mr' => 'Marathi',
1060 'ms' => 'Malay',
1061 'my' => 'Burmese',
1062 'ne' => 'Nepali',
1063 'nl' => 'Dutch',
1064 'no' => 'Norwegian',
1065 'pa' => 'Punjabi',
1066 'pl' => 'Polish',
1067 'pt' => 'Portuguese',
1068 'pt-BR' => 'Portuguese (Brazil)',
1069 'pt-PT' => 'Portuguese (Portugal)',
1070 'ro' => 'Romanian',
1071 'ru' => 'Russian',
1072 'si' => 'Sinhalese',
1073 'sk' => 'Slovak',
1074 'sl' => 'Slovenian',
1075 'sq' => 'Albanian',
1076 'sr' => 'Serbian',
1077 'sv' => 'Swedish',
1078 'sw' => 'Swahili',
1079 'ta' => 'Tamil',
1080 'te' => 'Telugu',
1081 'th' => 'Thai',
1082 'tr' => 'Turkish',
1083 'uk' => 'Ukrainian',
1084 'ur' => 'Urdu',
1085 'uz' => 'Uzbek',
1086 'vi' => 'Vietnamese',
1087 'zh' => 'Chinese',
1088 'zh-CN' => 'Chinese (Simplified)',
1089 'zh-HK' => 'Chinese (Hong Kong)',
1090 'zh-TW' => 'Chinese (Traditional)',
1091 'zu' => 'Zulu'
1092 ];
1093 if ($include_default === false) {
1094 array_shift($languages);
1095 }
1096
1097 //Detect if WPMl is active then add the option
1098 if (defined('ICL_SITEPRESS_VERSION') && Util::sbr_is_pro()) {
1099 $position = $include_default ? 2 : 1;
1100 $languages = array_merge(
1101 array_slice($languages, 0, $position),
1102 ['wpml' => 'Automatically by WPML'],
1103 array_slice($languages, $position)
1104 );
1105 }
1106
1107 return $languages;
1108 }
1109
1110 /**
1111 * Get Language for API call
1112 *
1113 * @since 1.0
1114 *
1115 * @return string
1116 */
1117
1118 public static function get_settings_language($settings)
1119 {
1120 $args = isset($settings['localization']) && $settings['localization'] !== 'default'
1121 ? $settings
1122 : wp_parse_args(get_option('sbr_settings', []), sbr_plugin_settings_defaults());
1123
1124 return $args['localization'];
1125 }
1126
1127 /**
1128 * Get Language for API call
1129 *
1130 * @since 1.0
1131 *
1132 * @return string
1133 */
1134 public static function get_api_call_language($settings)
1135 {
1136 $language = Util::sbr_is_pro()
1137 ? \SmashBalloon\Reviews\Pro\Helpers\SBR_WPML::get_current_language(Util::get_settings_language($settings))
1138 : Util::get_settings_language($settings);
1139
1140 /**
1141 * Filter the language code sent to the provider API (e.g. Google Places).
1142 * Lets a site map an unsupported locale to a supported one, e.g. a WPML
1143 * `es-mx` site forcing `es-419` for Latin American Spanish. SMASH-1617.
1144 * Read the current WPML language inside the callback (apply_filters
1145 * 'wpml_current_language') if you need per-page context.
1146 *
1147 * @since 2.6.6
1148 * @param string $language Resolved language code (or 'default').
1149 */
1150 return apply_filters('sbr_google_api_language', $language);
1151 }
1152
1153 /**
1154 * Map a locale / WPML language code to a code the Google Places API accepts.
1155 *
1156 * Google only honours codes from its supported list (get_translation_languages);
1157 * an unsupported value (e.g. WPML's `es-mx`) makes Google return reviews
1158 * untranslated. This normalises common regional codes to a supported one.
1159 * Spanish is special-cased: Spain dialects -> `es`, any other Spanish variant
1160 * (es-mx, es-ar, …) -> `es-419` (Latin America) — region-stripping alone would
1161 * wrongly land on Spain Spanish. Returns null when nothing supported matches,
1162 * so callers can fall back (never send a bogus code). SMASH-1617.
1163 *
1164 * @since 2.6.6
1165 * @param string|null $code Raw locale / WPML code (e.g. 'es-mx', 'pt-br', 'es_MX').
1166 * @return string|null A Google-supported code, or null if unmappable.
1167 */
1168 public static function map_wpml_to_google_language($code)
1169 {
1170 if (! is_string($code) || $code === '') {
1171 return null;
1172 }
1173
1174 $allowed = self::get_translation_languages();
1175
1176 // Already a supported code (exact match) — keep existing behaviour.
1177 if (isset($allowed[$code]) && $code !== 'default' && $code !== '') {
1178 return $code;
1179 }
1180
1181 $norm = strtolower(str_replace('_', '-', $code));
1182
1183 // Spanish: Spain -> es; every other variant (es-mx, es-ar, …) -> es-419.
1184 if ($norm === 'es' || $norm === 'es-es') {
1185 return 'es';
1186 }
1187 if (strpos($norm, 'es-') === 0) {
1188 return 'es-419';
1189 }
1190
1191 // Case-insensitive match for region variants Google does list (pt-br -> pt-BR).
1192 foreach (array_keys($allowed) as $supported) {
1193 if (strtolower($supported) === $norm) {
1194 return $supported;
1195 }
1196 }
1197
1198 // Strip the region and fall back to the base language if supported (fr-fr -> fr).
1199 $base = strtok($norm, '-');
1200 if ($base !== false && isset($allowed[$base])) {
1201 return $base;
1202 }
1203
1204 return null;
1205 }
1206
1207 /**
1208 * Is Plugin Pro
1209 *
1210 * @since 1.0
1211 *
1212 * @return boolean
1213 */
1214 public static function sbr_is_pro()
1215 {
1216 return defined('SBR_PRO') && SBR_PRO === true;
1217 }
1218
1219 /**
1220 * Check if user has Pro Plus tier
1221 *
1222 * @since 2.5.0
1223 *
1224 * @return boolean
1225 */
1226 public static function sbr_is_pro_plus()
1227 {
1228 if (!self::sbr_is_pro()) {
1229 return false;
1230 }
1231
1232 // Read from sbr_statuses where license_tier is written during license activation
1233 // This is consistent with AuthorizationStatusCheck::get_license_tier()
1234 $statuses = get_option('sbr_statuses', []);
1235 $license_tier = $statuses['license_tier'] ?? 0;
1236
1237 // Handle All Access Bundle special case (same as AuthorizationStatusCheck)
1238 if (
1239 isset($statuses['license_info']['item_name'])
1240 && $statuses['license_info']['item_name'] === 'All Access Bundle - All Plugins Unlimited'
1241 ) {
1242 $license_tier = 3;
1243 }
1244
1245 // Tiers: 1 = Pro Basic, 2 = Pro Plus, 3 = Elite
1246 // Pro Plus and Elite (tiers 2 and 3) have full access
1247 return (int) $license_tier >= 2;
1248 }
1249
1250
1251 /**
1252 * Get List of Upsell Modal Content
1253 *
1254 * @since 1.0
1255 *
1256 * @return array
1257 */
1258 public static function upsell_modal_content()
1259 {
1260 // Pro users (including Pro Plus) don't need provider upsell modals
1261 // Note: Review Alert Pro Plus upsells are handled separately in SBR_ReviewAlert_Builder
1262 if (Util::sbr_is_pro()) {
1263 return [];
1264 }
1265
1266 // Free users get all upsell modals
1267 return [
1268 'facebookProvider' => [
1269 'heading' => __('Upgrade to Pro to display Facebook reviews', 'reviews-feed'),
1270 'description' => __('Upgrade to our "Plus" tier to display reviews from the well known social media platform.', 'reviews-feed'),
1271 'image' => 'upsell-facebook.png',
1272 'buttons' => [
1273 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=facebook-modal&utm_content=LiteUsers50OFF',
1274 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=facebook-modal&utm_content=Upgrade',
1275 # 'demo' => 'https://smashballoon.com/reviews-feed/demo/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=responsive-modal&utm_content=ViewDemo'
1276 ],
1277
1278 'includeContent' => true
1279 ],
1280 'trustpilotProvider' => [
1281 'heading' => __('Upgrade to Pro to display TrustPilot reviews', 'reviews-feed'),
1282 'description' => __('Upgrade to our "Plus" tier to display reviews from the well known business review site.', 'reviews-feed'),
1283 'image' => 'upsell-trustpilot.png',
1284 'buttons' => [
1285 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=trustpilot-modal&utm_content=LiteUsers50OFF',
1286 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=trustpilot-modal&utm_content=Upgrade'
1287 ],
1288 'includeContent' => true
1289 ],
1290 'tripadvisorProvider' => [
1291 'heading' => __('Upgrade to Pro to display TripAdvisor reviews', 'reviews-feed'),
1292 'description' => __('Upgrade to our "Elite" tier to display reviews from the well known travel advice site.', 'reviews-feed'),
1293 'image' => 'upsell-tripadvisor.png',
1294 'buttons' => [
1295 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=tripadvisor-modal&utm_content=LiteUsers50OFF',
1296 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=tripadvisor-modal&utm_content=Upgrade'
1297 ],
1298 'includeContent' => true
1299 ],
1300 'wordpress.orgProvider' => [
1301 'heading' => __('Upgrade to Pro to display WordPress.org reviews', 'reviews-feed'),
1302 'description' => __('Upgrade to our "Elite" tier to display reviews for plugins and themes.', 'reviews-feed'),
1303 'image' => 'upsell-wordpress.org.png',
1304 'buttons' => [
1305 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=wordpressorg-modal&utm_content=LiteUsers50OFF',
1306 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=wordpressorg-modal&utm_content=Upgrade'
1307 ],
1308 'includeContent' => true
1309 ],
1310 'woocommerceProvider' => [
1311 'heading' => __('Upgrade to Pro to display WooCommerce reviews', 'reviews-feed'),
1312 'description' => __('Upgrade to our "Plus" tier to display product reviews from your WooCommerce store.', 'reviews-feed'),
1313 'image' => 'upsell-woocommerce.png',
1314 'buttons' => [
1315 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=woocommerce-modal&utm_content=LiteUsers50OFF',
1316 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=woocommerce-modal&utm_content=Upgrade'
1317 ],
1318 'includeContent' => true
1319 ],
1320 'eddProvider' => [
1321 'heading' => __('Upgrade to Pro to display Easy Digital Downloads reviews', 'reviews-feed'),
1322 'description' => __('Upgrade to our "Plus" tier to display download reviews from your EDD store.', 'reviews-feed'),
1323 'image' => 'upsell-edd.png',
1324 'buttons' => [
1325 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=edd-modal&utm_content=LiteUsers50OFF',
1326 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=edd-modal&utm_content=Upgrade'
1327 ],
1328 'includeContent' => true
1329 ],
1330 'airbnbProvider' => [
1331 'heading' => __('Upgrade to Pro to display Airbnb reviews', 'reviews-feed'),
1332 'description' => __('Upgrade to our "Elite" tier to display reviews from the popular accommodation platform.', 'reviews-feed'),
1333 'image' => 'upsell-airbnb.png',
1334 'buttons' => [
1335 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=airbnb-modal&utm_content=LiteUsers50OFF',
1336 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=airbnb-modal&utm_content=Upgrade'
1337 ],
1338 'includeContent' => true
1339 ],
1340 'bookingProvider' => [
1341 'heading' => __('Upgrade to Pro to display Booking.com reviews', 'reviews-feed'),
1342 'description' => __('Upgrade to our "Elite" tier to display reviews from the leading travel booking site.', 'reviews-feed'),
1343 'image' => 'upsell-booking.png',
1344 'buttons' => [
1345 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=booking-modal&utm_content=LiteUsers50OFF',
1346 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=booking-modal&utm_content=Upgrade'
1347 ],
1348 'includeContent' => true
1349 ],
1350 'aliexpressProvider' => [
1351 'heading' => __('Upgrade to Pro to display AliExpress reviews', 'reviews-feed'),
1352 'description' => __('Upgrade to our "Plus" tier to display product reviews from the global marketplace.', 'reviews-feed'),
1353 'image' => 'upsell-aliexpress.png',
1354 'buttons' => [
1355 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=aliexpress-modal&utm_content=LiteUsers50OFF',
1356 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=aliexpress-modal&utm_content=Upgrade'
1357 ],
1358 'includeContent' => true
1359 ],
1360 'carouselModal' => [
1361 'heading' => __('Upgrade to Pro to get Carousel layout', 'reviews-feed'),
1362 'description' => __('An eye-catching rotating slider of your videos to add extra content in minimal space on your website.', 'reviews-feed'),
1363 'image' => 'upsell-carousel.png',
1364 'buttons' => [
1365 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=carousel-modal&utm_content=LiteUsers50OFF',
1366 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=carousel-modal&utm_content=Upgrade'
1367 ],
1368 'includeContent' => true
1369 ],
1370 'moreReviewsModal' => [
1371 'heading' => __('Upgrade to Pro to display more reviews', 'reviews-feed'),
1372 'description' => __('More layout settings to customize the look and feel of your reviews even more.', 'reviews-feed'),
1373 'image' => 'upsell-morereviews.png',
1374 'buttons' => [
1375 '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',
1376 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=num-reviews-modal&utm_content=Upgrade'
1377 ],
1378 'includeContent' => true
1379 ],
1380 'averageRatingModal' => [
1381 'heading' => __('Upgrade to Pro to display average rating', 'reviews-feed'),
1382 'description' => __('Boost social proof to make more sales conversions with the number of ratings and an average rating.', 'reviews-feed'),
1383 'image' => 'upsell-averagerating.png',
1384 'buttons' => [
1385 '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',
1386 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=average-rating-modal&utm_content=Upgrade'
1387 ],
1388 'includeContent' => true
1389 ],
1390 'loadMoreModal' => [
1391 'heading' => __('Upgrade to Pro to add load more functionality', 'reviews-feed'),
1392 'description' => __('Overwhelm (in a good way) your visitors with additional reviews loaded on the page with a click.', 'reviews-feed'),
1393 'image' => 'upsell-loadmore.png',
1394 'buttons' => [
1395 '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',
1396 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=load-more-modal&utm_content=Upgrade'
1397 ],
1398 'includeContent' => true
1399 ],
1400 'reviewsMediaModal' => [
1401 'heading' => __('Upgrade to Pro to add images', 'reviews-feed'),
1402 'description' => __('Display images from Yelp and Tripadvisor reviews.', 'reviews-feed'),
1403 'image' => 'upsell-reviewsmedia.png',
1404 'buttons' => [
1405 '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',
1406 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=template-modal&utm_content=Upgrade'
1407 ],
1408 'includeContent' => true
1409 ],
1410 'authorImageModal' => [
1411 'heading' => __('Upgrade to Pro to display author images', 'reviews-feed'),
1412 'description' => __('Build brand trust with positive reviews from real customers.', 'reviews-feed'),
1413 'image' => 'upsell-authorimage.png',
1414 'buttons' => [
1415 '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',
1416 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=author-avatar-modal&utm_content=Upgrade'
1417 ],
1418 'includeContent' => true
1419 ],
1420 'filtersModal' => [
1421 'heading' => __('Upgrade to Pro to filter your reviews', 'reviews-feed'),
1422 'description' => __('Show only the most positive reviews and build brand trust with review filtering.', 'reviews-feed'),
1423 'image' => 'upsell-filters.png',
1424 'buttons' => [
1425 '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',
1426 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=star-filter-modal&utm_content=Upgrade'
1427 ],
1428 'includeContent' => true
1429 ],
1430 'moderationModal' => [
1431 'heading' => __('Upgrade to Pro to moderate your reviews', 'reviews-feed'),
1432 'description' => __('Take complete control of what reviews show in the feed using keyword filters and a visual moderation system.', 'reviews-feed'),
1433 'image' => 'upsell-moderation.png',
1434 'buttons' => [
1435 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=moderation-modal&utm_content=LiteUsers50OFF',
1436 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=moderation-modal&utm_content=Upgrade'
1437 ],
1438 'includeContent' => true
1439 ],
1440 'templateModal' => [
1441 'heading' => __('Upgrade to Pro to get one-click templates!', 'reviews-feed'),
1442 'description' => __('Quickly create and preview new feeds with pre-configured options based on popular feed types.', 'reviews-feed'),
1443 'image' => 'upsell-template.png',
1444 'buttons' => [
1445 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=template-modal&utm_content=LiteUsers50OFF',
1446 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=template-modal&utm_content=Upgrade'
1447 ],
1448 'includeContent' => true
1449 ],
1450 'responsiveModal' => [
1451 'heading' => __('Upgrade to Pro for responsive layouts', 'reviews-feed'),
1452 'description' => __('Take control of your feed layouts by customizing number of reviews & columns', 'reviews-feed'),
1453 'image' => 'upsell-responsive.png',
1454 'buttons' => [
1455 'lite' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=all-feeds&utm_medium=responsive-modal&utm_content=LiteUsers50OFF',
1456 'upgrade' => 'https://smashballoon.com/reviews-feed/reviews-lite-upgrade/?utm_campaign=reviews-free&utm_source=customizer&utm_medium=responsive-modal&utm_content=Upgrade'
1457 ],
1458 'includeContent' => true
1459 ],
1460 // Review Alert Upsell Modals
1461 'reviewAlertFeature' => [
1462 'heading' => __('Upgrade to Pro for Review Alerts', 'reviews-feed'),
1463 'description' => __('Display eye-catching review popups that cycle through your best reviews to boost social proof and conversions.', 'reviews-feed'),
1464 'image' => 'upsell-review-alert.png',
1465 'buttons' => [
1466 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-feature-modal&utm_content=LiteUsers50OFF',
1467 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-feature-modal&utm_content=Upgrade'
1468 ],
1469 'includeContent' => true
1470 ],
1471 'reviewAlertRecentReviews' => [
1472 'heading' => __('Upgrade to Pro for Recent Reviews popup', 'reviews-feed'),
1473 'description' => __('Cycle through your individual reviews one by one to showcase real customer feedback and build trust.', 'reviews-feed'),
1474 'image' => 'upsell-review-alert.png',
1475 'buttons' => [
1476 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-recent-reviews-modal&utm_content=LiteUsers50OFF',
1477 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-recent-reviews-modal&utm_content=Upgrade'
1478 ],
1479 'includeContent' => true
1480 ],
1481 'reviewAlertVariations' => [
1482 'heading' => __('Upgrade to Pro for more popup styles', 'reviews-feed'),
1483 'description' => __('Unlock V2 and V3 style variations for your notification popups with unique layouts and designs.', 'reviews-feed'),
1484 'image' => 'upsell-review-alert.png',
1485 'buttons' => [
1486 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-variations-modal&utm_content=LiteUsers50OFF',
1487 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-variations-modal&utm_content=Upgrade'
1488 ],
1489 'includeContent' => true
1490 ],
1491 'reviewAlertDarkTheme' => [
1492 'heading' => __('Upgrade to Pro for dark theme popups', 'reviews-feed'),
1493 'description' => __('Match your site\'s dark mode with elegant dark-themed notification popups.', 'reviews-feed'),
1494 'image' => 'upsell-review-alert.png',
1495 'buttons' => [
1496 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-dark-theme-modal&utm_content=LiteUsers50OFF',
1497 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-dark-theme-modal&utm_content=Upgrade'
1498 ],
1499 'includeContent' => true
1500 ],
1501 'reviewAlertCustomColor' => [
1502 'heading' => __('Upgrade to Pro for custom popup colors', 'reviews-feed'),
1503 'description' => __('Customize your popup accent color to perfectly match your brand identity.', 'reviews-feed'),
1504 'image' => 'upsell-review-alert.png',
1505 'buttons' => [
1506 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-custom-color-modal&utm_content=LiteUsers50OFF',
1507 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-custom-color-modal&utm_content=Upgrade'
1508 ],
1509 'includeContent' => true
1510 ],
1511 'reviewAlertMultiple' => [
1512 'heading' => __('Upgrade to Pro Plus for multiple popups', 'reviews-feed'),
1513 'description' => __('Create unlimited notification popups and display different popups on different pages.', 'reviews-feed'),
1514 'image' => 'upsell-review-alert.png',
1515 'buttons' => [
1516 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-multiple-modal&utm_content=LiteUsers50OFF',
1517 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-multiple-modal&utm_content=Upgrade'
1518 ],
1519 'includeContent' => true
1520 ],
1521 'reviewAlertTargeting' => [
1522 'heading' => __('Upgrade to Pro for page targeting', 'reviews-feed'),
1523 'description' => __('Control exactly where your notification popups appear with specific page targeting and exclusions.', 'reviews-feed'),
1524 'image' => 'upsell-review-alert.png',
1525 'buttons' => [
1526 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-targeting-modal&utm_content=LiteUsers50OFF',
1527 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-targeting-modal&utm_content=Upgrade'
1528 ],
1529 'includeContent' => true
1530 ],
1531 'reviewAlertBranding' => [
1532 'heading' => __('Remove Smash Balloon Branding', 'reviews-feed'),
1533 'description' => __('Present a clean, professional look by removing the \'Powered by\' badge from your notification popups.', 'reviews-feed'),
1534 'image' => 'upsell-review-alert.png',
1535 'buttons' => [
1536 'lite' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-branding-modal&utm_content=LiteUsers50OFF',
1537 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-branding-modal&utm_content=Upgrade',
1538 'learnMore' => 'https://smashballoon.com/reviews-feed/?utm_campaign=reviews-free&utm_source=review-alert&utm_medium=popup-branding-modal&utm_content=LearnMore'
1539 ],
1540 'includeContent' => true
1541 ]
1542 ];
1543 }
1544
1545 /**
1546 * Get Pro Plus upsell content for Pro users
1547 *
1548 * Returns upsell modals for features that require Pro Plus tier.
1549 *
1550 * @since 2.5.0
1551 *
1552 * @return array
1553 */
1554 public static function get_pro_plus_upsell_content()
1555 {
1556 return [
1557 'reviewAlertMultiple' => [
1558 'heading' => __('Upgrade to Pro Plus for multiple popups', 'reviews-feed'),
1559 'description' => __('Create unlimited notification popups and display different popups on different pages.', 'reviews-feed'),
1560 'image' => 'upsell-review-alert.png',
1561 'buttons' => [
1562 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-pro&utm_source=review-alert&utm_medium=popup-multiple-modal&utm_content=UpgradeToProPlus'
1563 ],
1564 'includeContent' => false
1565 ],
1566 'reviewAlertTargeting' => [
1567 'heading' => __('Upgrade to Pro Plus for page targeting', 'reviews-feed'),
1568 'description' => __('Control exactly where your notification popups appear with specific page targeting and exclusions.', 'reviews-feed'),
1569 'image' => 'upsell-review-alert.png',
1570 'buttons' => [
1571 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-pro&utm_source=review-alert&utm_medium=popup-targeting-modal&utm_content=UpgradeToProPlus'
1572 ],
1573 'includeContent' => false
1574 ],
1575 'reviewAlertBranding' => [
1576 'heading' => __('Remove Smash Balloon Branding', 'reviews-feed'),
1577 'description' => __('Present a clean, professional look by removing the \'Powered by\' badge from your notification popups.', 'reviews-feed'),
1578 'image' => 'upsell-review-alert.png',
1579 'buttons' => [
1580 'upgrade' => 'https://smashballoon.com/pricing/reviews-feed/?utm_campaign=reviews-pro&utm_source=review-alert&utm_medium=popup-branding-modal&utm_content=UpgradeToProPlus',
1581 'learnMore' => 'https://smashballoon.com/reviews-feed/?utm_campaign=reviews-pro&utm_source=review-alert&utm_medium=popup-branding-modal&utm_content=LearnMore'
1582 ],
1583 'includeContent' => true
1584 ]
1585 ];
1586 }
1587
1588 /**
1589 * Get List of Upsell Sidebar Cards
1590 *
1591 * @since 1.1
1592 *
1593 * @return array
1594 */
1595 public static function sidebar_upsell_cards()
1596 {
1597 if (Util::sbr_is_pro()) {
1598 return [];
1599 }
1600 return [
1601 [
1602 'heading' => __('Display images', 'reviews-feed'),
1603 'description' => __('With the Pro version enable images like avatars and review photos', 'reviews-feed'),
1604 'image' => 'upswidget-images.png',
1605 'modal' => 'authorImageModal'
1606 ],
1607 [
1608 'heading' => __('Get carousel layout', 'reviews-feed'),
1609 'description' => __('Show your reviews in a neat carousel with Review Feed Pro', 'reviews-feed'),
1610 'image' => 'upswidget-carousel.png',
1611 'modal' => 'carouselModal'
1612 ],
1613 [
1614 'heading' => __('Change your feed style with one-click templates!', 'reviews-feed'),
1615 'description' => __('Over 12 templates to choose from', 'reviews-feed'),
1616 'image' => 'upswidget-templates.png',
1617 'modal' => 'templateModal'
1618 ],
1619 [
1620 'heading' => __('Only show selected reviews with Moderation mode', 'reviews-feed'),
1621 'description' => __('Hide or show only certain reviews', 'reviews-feed'),
1622 'image' => 'upswidget-moderation.png',
1623 'modal' => 'moderationModal'
1624 ]
1625 ];
1626 }
1627
1628
1629 /**
1630 * Coerce a review's `provider`, `reviewer` and `source` into the array shapes
1631 * every reader expects ($review['provider']['name'], $review['reviewer']['name'],
1632 * $review['source']['id'], …). The relay can emit these as scalars (e.g.
1633 * `provider` => 'google', or an error-shaped payload yielding a scalar
1634 * `reviewer`/`source`), and PHP 7-era caches stored them that way in
1635 * `json_data`. On PHP 8.0+ a direct nested read on a scalar fatals with
1636 * "Cannot access offset of type string on string".
1637 *
1638 * This is the single source of truth used by SinglePostCache (both
1639 * constructors) and every raw/DB-decoded read site: the dedup key build in
1640 * PostAggregator::remove_duplicated_posts_list (front-end render, Feed.php:172)
1641 * reads source['id'] + reviewer['name'] + provider['name'] off the raw post,
1642 * parse_single_review reads reviewer/provider, and
1643 * SBR_Feed_Saver_Manager::duplicate_collection reads reviewer/source.
1644 *
1645 * SMASH-1578 guarded whole-review + source shapes at the cache loops, SMASH-1587
1646 * added provider; this also covers a scalar reviewer/source that the
1647 * is_array($single_review)-only cache guard lets through to store()/dedup
1648 * (reachable on WPSA-63160's associative-keyed payload). Healthy array data is
1649 * left intact — missing keys are filled with empty strings (via array union),
1650 * so the dedup key degrades to an empty segment instead of crashing.
1651 *
1652 * @param mixed $review
1653 * @return mixed Untouched when not an array (callers' is_array guards handle that).
1654 */
1655 public static function normalize_review_shape($review)
1656 {
1657 if (!is_array($review)) {
1658 return $review;
1659 }
1660
1661 if (isset($review['provider']) && is_string($review['provider'])) {
1662 $review['provider'] = ['name' => $review['provider']];
1663 } elseif (!isset($review['provider']) || !is_array($review['provider'])) {
1664 $review['provider'] = ['name' => ''];
1665 }
1666 if (!isset($review['reviewer']) || !is_array($review['reviewer'])) {
1667 $review['reviewer'] = [];
1668 }
1669 if (!isset($review['source']) || !is_array($review['source'])) {
1670 $review['source'] = [];
1671 }
1672 // Image containers iterated by resize_images() + add_local_image_urls().
1673 // A scalar here would fatal the foreach on PHP 8. Coerce a present
1674 // non-array to [] (leave absent untouched — they're optional). Element
1675 // shape is guarded at the loop sites (a flat URL-string element).
1676 foreach (['media', 'reviews_photos', 'reviewer_photos'] as $image_key) {
1677 if (isset($review[$image_key]) && !is_array($review[$image_key])) {
1678 $review[$image_key] = [];
1679 }
1680 }
1681
1682 // Guarantee the exact keys every reader assumes are present and string,
1683 // so a scalar/partial provider, reviewer or source degrades to empty
1684 // segments instead of "Cannot access offset…" fatals or "Undefined array
1685 // key" notices in the dedup key build / parse_single_review / preview
1686 // backfill (SMASH-1587 + PR #484 Copilot review + WPSA-63160 follow-up).
1687 $review['provider'] += ['name' => ''];
1688 $review['reviewer'] += ['name' => '', 'avatar' => ''];
1689 $review['source'] += ['id' => '', 'url' => ''];
1690 if (!is_string($review['provider']['name'])) {
1691 $review['provider']['name'] = '';
1692 }
1693 if (!is_string($review['reviewer']['name'])) {
1694 $review['reviewer']['name'] = '';
1695 }
1696 if (!is_string($review['source']['id'])) {
1697 // Preserve a numeric id (cast), but never (string)-cast an array/object —
1698 // that would emit an "Array to string conversion" notice (PR #482 Copilot).
1699 $review['source']['id'] = is_scalar($review['source']['id'])
1700 ? (string) $review['source']['id']
1701 : '';
1702 }
1703
1704 return $review;
1705 }
1706
1707 /**
1708 * Narrow a payload value to a string before handing it to a sanitiser.
1709 *
1710 * Shortcode attributes and JSON-decoded review payloads are `mixed`. A blind
1711 * `(string)` cast is what level 9 objects to, and rightly — an array warns and an
1712 * object without __toString() fatals. Non-scalars become ''.
1713 *
1714 * @param mixed $value
1715 * @return string
1716 */
1717 public static function payload_string($value): string
1718 {
1719 if (is_string($value)) {
1720 return $value;
1721 }
1722 if (is_scalar($value)) {
1723 return (string) $value;
1724 }
1725 return '';
1726 }
1727
1728 /**
1729 * Sanitise a rating, preserving Facebook's 'positive'/'negative' sentinels.
1730 *
1731 * Parser::get_rating() maps those two strings to 5 and 1, and maps anything falsy
1732 * to 1 — so coercing them (absint('positive') === 0) turns a 5-star recommendation
1733 * into 1 star. Numerics pass through uncast because
1734 * Feed::sort_array_byrating_and_date() compares stored ratings with ===, which a
1735 * 5 -> 5.0 change would break. Values are NOT clamped to 0-5: that matches the
1736 * previous behaviour, and every render sink already bounds the star loop.
1737 *
1738 * @param mixed $rating
1739 * @return float|int|string
1740 */
1741 public static function sanitize_rating($rating)
1742 {
1743 if ($rating === 'positive' || $rating === 'negative') {
1744 return $rating;
1745 }
1746 if (is_numeric($rating)) {
1747 return $rating;
1748 }
1749 // absint(), not 0 — "3 stars" must stay 3. is_numeric('5 ') is false on PHP 7.4
1750 // (our floor), so trailing whitespace reaches this branch too.
1751 return absint(trim(self::payload_string($rating)));
1752 }
1753
1754 /**
1755 * Normalise a review time, keeping the non-numeric form.
1756 *
1757 * A date string is supported — SB_Analytics branches on is_numeric() — and
1758 * FeedDisplay drops the date element entirely on a falsy time, so coercing to 0
1759 * makes the date vanish rather than merely look wrong.
1760 *
1761 * @param mixed $time
1762 * @return int|string
1763 */
1764 public static function sanitize_time($time)
1765 {
1766 if (is_numeric($time)) {
1767 return absint($time);
1768 }
1769 return sanitize_text_field(self::payload_string($time));
1770 }
1771
1772 /**
1773 * Sanitise a provider slug: sanitize_key() semantics, but the dot survives.
1774 *
1775 * The dot matters because 'wordpress.org' is a real slug (WordpressOrg::$name)
1776 * compared as a literal at RemoteRequest:189, Util:274/:319 and
1777 * SBR_Feed_Saver_Manager:743. Everything else sanitize_key() does is load-bearing
1778 * and kept: downstream provider checks are lowercase case-sensitive literals, and
1779 * FeedDisplay::provider_icon_url() concatenates this value into
1780 * `assets/icons/{$provider}-provider.svg`, so the charset has to exclude `/`.
1781 *
1782 * @param mixed $provider
1783 * @return string
1784 */
1785 public static function sanitize_provider_slug($provider): string
1786 {
1787 $slug = strtolower(trim(self::payload_string($provider)));
1788 $slug = (string) preg_replace('/[^a-z0-9._\-]/', '', $slug);
1789 // '..' survives the charset above and is never part of a real slug.
1790 return str_replace('..', '', $slug);
1791 }
1792
1793 /**
1794 * Narrow an avatar value to a safe URL.
1795 *
1796 * Deliberately a thin wrapper: esc_url_raw() already handles every obfuscation of
1797 * a dangerous scheme, including the entity-encoded ones. Do not reintroduce a
1798 * scheme test to preserve relative paths — the render site (author.php:25) wraps
1799 * the value in esc_url(), which prepends the same `http://`, so a scheme-less
1800 * avatar never worked end-to-end anyway.
1801 *
1802 * @param mixed $url
1803 * @return string
1804 */
1805 public static function sanitize_avatar_url($url): string
1806 {
1807 return esc_url_raw(trim(wp_strip_all_tags(self::payload_string($url))));
1808 }
1809
1810 /**
1811 * Transform Single Review for storing purposes
1812 *
1813 * @since 1.4
1814 *
1815 * @return array
1816 */
1817 public static function parse_single_review($review, $provider_id, $review_id)
1818 {
1819 // Provider can arrive as a scalar slug (SMASH-1587); normalize before the
1820 // $review['provider']['name'] read below so it can't fatal on PHP 8.
1821 $review = self::normalize_review_shape($review);
1822 $name = $review['reviewer']['name'];
1823 $name_array = explode(' ', $name);
1824 $first_name = isset($review['reviewer']['first_name']) ? $review['reviewer']['first_name'] : $name_array[0];
1825 $last_name = isset($review['reviewer']['last_name']) ? $review['reviewer']['last_name'] : (isset($name_array[1]) ? $name_array[1] : '');
1826
1827 $sanitized_review = [
1828 'time' => $review['time'],
1829 'rating' => $review['rating'],
1830 'provider_id' => $provider_id,
1831 'review_id' => $review_id,
1832 'text' => $review['text'],
1833 'title' => isset($review['title']) ? $review['title'] : substr($review['text'], 0, 40),
1834 'reviewer' => [
1835 'name' => $name,
1836 'first_name' => $first_name,
1837 'last_name' => $last_name,
1838 'avatar' => $review['reviewer']['avatar']
1839 ],
1840 'provider' => [
1841 'name' => $review['provider']['name']
1842 ],
1843 'source' => [
1844 'id' => $provider_id,
1845 'url' => ''
1846 ]
1847 ];
1848 return $sanitized_review;
1849 }
1850
1851
1852 public static function is_facebook_collection_post($post)
1853 {
1854 return ( isset($post['provider']) && isset($post['provider']['name']) && $post['provider']['name'] === 'facebook' && isset($post['provider_id']) && strpos($post['provider_id'], 'collection') !== false ) === true;
1855 }
1856
1857 /**
1858 * Convert Object to Array
1859 *
1860 * @return array
1861 *
1862 * @since 1.0
1863 */
1864 public static function object_to_array($data)
1865 {
1866 if (is_object($data)) {
1867 $data = json_decode(json_encode($data), true);
1868 }
1869 return $data;
1870 }
1871
1872 /**
1873 * Convert Object to Array
1874 *
1875 * @return array
1876 *
1877 * @since 1.0
1878 */
1879 public static function get_free_retriever_data()
1880 {
1881 if (Util::sbr_is_pro()) {
1882 $retriever = new \SmashBalloon\Reviews\Pro\Utils\FreeRetriever();
1883 } else {
1884 $retriever = new \SmashBalloon\Reviews\Common\Utils\FreeRetriever();
1885 }
1886 return $retriever->get_settings();
1887 }
1888
1889 /**
1890 * Get Feeds Settings
1891 *
1892 * @since 1.0
1893 *
1894 * @return string
1895 */
1896 public static function get_settings_page_errors()
1897 {
1898 $output = '## ERROR LOGS: ## </br></br>';
1899 $errors = SBR_Error_Handler::get_errors();
1900 $errors = array_reverse($errors);
1901 foreach ($errors as $error) {
1902 $output .= json_encode($error);
1903 $output .= '</br></br>';
1904 }
1905 $output .= '</br>';
1906
1907 return $output;
1908 }
1909
1910 /**
1911 * Get Local Images/Avatar
1912 * Upload folder name
1913 *
1914 * @since 2.0
1915 *
1916 * @return string
1917 */
1918 public static function get_upload_folder_name()
1919 {
1920 $upload = wp_upload_dir();
1921 $folder = trailingslashit($upload['basedir']) . trailingslashit('sbr-feed-images');
1922 return $folder;
1923 }
1924
1925 public static function should_store_local_images()
1926 {
1927 $settings = get_option('sbr_settings', sbr_plugin_settings_defaults());
1928 if (! is_array($settings)) {
1929 $settings = sbr_plugin_settings_defaults();
1930 }
1931 return !empty($settings['optimize_images']) ? $settings['optimize_images'] : true;
1932 }
1933
1934 /**
1935 * Convert an ISO-3166 alpha-2 country code to its regional-indicator flag emoji.
1936 *
1937 * Shared by the AliExpress feed author element (post-elements/author.php) and
1938 * the Review Alerts popup (review-alerts/popup.php) so both render the same
1939 * glyph from the same rules. Returns '' when the input is not a 2-letter code,
1940 * or when mbstring is unavailable (some shared hosts ship without it) — callers
1941 * simply skip the flag rather than fataling the render.
1942 *
1943 * @param string $cc Country code (e.g. "US", "de").
1944 * @return string The flag emoji, or '' if not derivable.
1945 */
1946 public static function country_flag_emoji($cc)
1947 {
1948 $cc = trim((string) $cc);
1949 // ISO-3166 alpha-2 only. ASCII regex, not ctype_alpha() — ctype_* is
1950 // locale-dependent and can accept non-ASCII letters, which would flow
1951 // into ord() and produce wrong codepoints (copilot #467).
1952 if (! preg_match('/^[A-Za-z]{2}$/', $cc) || ! function_exists('mb_chr')) {
1953 return '';
1954 }
1955 $up = strtoupper($cc);
1956 $offset = 0x1F1E6 - ord('A');
1957 return mb_chr(ord($up[0]) + $offset, 'UTF-8') . mb_chr(ord($up[1]) + $offset, 'UTF-8');
1958 }
1959
1960 }
1961