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(' ', $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 |