| 1 |
<?php |
| 2 |
/** |
| 3 |
* Ultimate Post Kit Remote Data Handler |
| 4 |
* |
| 5 |
* Handles remote API data loading with proper caching and background processing |
| 6 |
* to prevent blocking admin pages. |
| 7 |
*/ |
| 8 |
|
| 9 |
namespace UltimatePostKit\SetupWizard; |
| 10 |
|
| 11 |
if (!defined('ABSPATH')) { |
| 12 |
exit; |
| 13 |
} |
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
class Remote_Data_Handler { |
| 18 |
|
| 19 |
/** |
| 20 |
* Cache duration in seconds (7 days) |
| 21 |
*/ |
| 22 |
const CACHE_DURATION = 7 * DAY_IN_SECONDS; |
| 23 |
|
| 24 |
/** |
| 25 |
* Transient key for remote plugins data |
| 26 |
*/ |
| 27 |
const CACHE_KEY = 'bdt_remote_plugins_data'; |
| 28 |
|
| 29 |
/** |
| 30 |
* Bundled logo file name for each plugin slug. |
| 31 |
* |
| 32 |
* These ship with the plugin (assets/images/others-plugin-logo/) so the plugin |
| 33 |
* cards render our own branded artwork instead of the wordpress.org icon, and |
| 34 |
* still show something for plugins whose .org listing has no icon at all. |
| 35 |
* The key is the wordpress.org slug; the value is the file base name. |
| 36 |
* |
| 37 |
* @var array<string, string> |
| 38 |
*/ |
| 39 |
const LOCAL_PLUGIN_LOGOS = [ |
| 40 |
'bdthemes-element-pack-lite' => 'element-pack', |
| 41 |
'bdthemes-prime-slider-lite' => 'prime-slider', |
| 42 |
'ultimate-post-kit' => 'ultimate-post-kit', |
| 43 |
'ultimate-store-kit' => 'ultimate-store-kit', |
| 44 |
'zoloblocks' => 'zoloblocks', |
| 45 |
'pixel-gallery' => 'pixel-gallery', |
| 46 |
'live-copy-paste' => 'live-copy-paste', |
| 47 |
'spin-wheel' => 'spin-wheel', |
| 48 |
'ai-image' => 'ai-image', |
| 49 |
'dark-reader' => 'dark-reader', |
| 50 |
'ar-viewer' => 'ar-viewer', |
| 51 |
'smart-admin-assistant' => 'smart-admin-assistant', |
| 52 |
'website-accessibility' => 'one-accessibility', |
| 53 |
'launch-guard' => 'launch-guard', |
| 54 |
'sigma-forms' => 'sigma-forms', |
| 55 |
'sigma-media-manager' => 'sigma-media-manager', |
| 56 |
'sigma-store-locator' => 'sigma-store-locator', |
| 57 |
'swift-checkout' => 'swift-checkout', |
| 58 |
]; |
| 59 |
|
| 60 |
/** |
| 61 |
* Resolve the bundled logo URL for a plugin slug. |
| 62 |
* |
| 63 |
* @param string $slug Plugin slug. |
| 64 |
* @return string Logo URL, or an empty string when the slug has no bundled logo. |
| 65 |
*/ |
| 66 |
public static function get_local_plugin_logo( $slug ) { |
| 67 |
if ( ! is_string( $slug ) || '' === $slug || ! isset( self::LOCAL_PLUGIN_LOGOS[ $slug ] ) ) { |
| 68 |
return ''; |
| 69 |
} |
| 70 |
|
| 71 |
$file = self::LOCAL_PLUGIN_LOGOS[ $slug ] . '.png'; |
| 72 |
|
| 73 |
// Only advertise the file if it actually shipped, so a trimmed build falls |
| 74 |
// back to the remote icon rather than rendering a broken image. |
| 75 |
if ( defined( 'BDTUPK_PATH' ) && ! file_exists( BDTUPK_PATH . 'assets/images/others-plugin-logo/' . $file ) ) { |
| 76 |
return ''; |
| 77 |
} |
| 78 |
|
| 79 |
return BDTUPK_ASSETS_URL . 'images/others-plugin-logo/' . $file; |
| 80 |
} |
| 81 |
|
| 82 |
/** |
| 83 |
* Cron hook name for background fetch |
| 84 |
*/ |
| 85 |
const CRON_HOOK = 'bdt_fetch_remote_plugins_cron'; |
| 86 |
|
| 87 |
/** |
| 88 |
* Initialize the remote data handler |
| 89 |
*/ |
| 90 |
public static function init() { |
| 91 |
add_action('init', [__CLASS__, 'schedule_cron']); |
| 92 |
add_action(self::CRON_HOOK, [__CLASS__, 'cron_fetch_plugins']); |
| 93 |
// Admin-only plugin-install data; never expose to unauthenticated visitors. |
| 94 |
add_action('wp_ajax_upk_get_plugins', [__CLASS__, 'ajax_get_plugins']); |
| 95 |
} |
| 96 |
|
| 97 |
/** |
| 98 |
* WP-Cron callback for fetching plugins |
| 99 |
*/ |
| 100 |
public static function cron_fetch_plugins() { |
| 101 |
self::fetch_remote_plugins_now(); |
| 102 |
} |
| 103 |
|
| 104 |
/** |
| 105 |
* Check if we're on the Ultimate Post Kit options page |
| 106 |
* |
| 107 |
* @return bool True if on Ultimate Post Kit options page |
| 108 |
*/ |
| 109 |
public static function is_element_pack_page() { |
| 110 |
if (!is_admin()) { |
| 111 |
return false; |
| 112 |
} |
| 113 |
|
| 114 |
// Check if this is an AJAX request for our plugins |
| 115 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check of the AJAX action name, no form data processed. |
| 116 |
if (wp_doing_ajax() && isset($_REQUEST['action'])) { |
| 117 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check of the AJAX action name, no form data processed. |
| 118 |
$action = sanitize_text_field(wp_unslash($_REQUEST['action'])); |
| 119 |
if (in_array($action, ['upk_get_plugins'])) { |
| 120 |
return true; |
| 121 |
} |
| 122 |
} |
| 123 |
|
| 124 |
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only check of the current admin page slug, no form data processed. |
| 125 |
$page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; |
| 126 |
return $page === 'ultimate_post_kit_options'; |
| 127 |
} |
| 128 |
|
| 129 |
/** |
| 130 |
* Get remote plugins data from cache |
| 131 |
* |
| 132 |
* @return array Cached plugins data or empty array if not available |
| 133 |
*/ |
| 134 |
public static function get_remote_plugins() { |
| 135 |
$cached_data = get_transient(self::CACHE_KEY); |
| 136 |
|
| 137 |
if ($cached_data !== false) { |
| 138 |
return $cached_data; |
| 139 |
} |
| 140 |
|
| 141 |
// If no cache exists, schedule background fetch and return empty array |
| 142 |
self::schedule_remote_fetch(); |
| 143 |
|
| 144 |
return []; |
| 145 |
} |
| 146 |
|
| 147 |
/** |
| 148 |
* Schedule a background fetch via WP-Cron |
| 149 |
* |
| 150 |
* @return bool True if successfully scheduled |
| 151 |
*/ |
| 152 |
public static function schedule_remote_fetch() { |
| 153 |
// Schedule to run immediately if not already scheduled |
| 154 |
if (!wp_next_scheduled(self::CRON_HOOK)) { |
| 155 |
wp_schedule_single_event(time(), self::CRON_HOOK); |
| 156 |
return true; |
| 157 |
} |
| 158 |
|
| 159 |
return false; |
| 160 |
} |
| 161 |
|
| 162 |
/** |
| 163 |
* Fetch remote plugins data immediately (for background processing only) |
| 164 |
* |
| 165 |
* @return array|false Plugins data or false on failure |
| 166 |
*/ |
| 167 |
public static function fetch_remote_plugins_now() { |
| 168 |
// Define plugin slugs to fetch |
| 169 |
$plugin_slugs = [ |
| 170 |
'bdthemes-element-pack-lite', |
| 171 |
'bdthemes-prime-slider-lite', |
| 172 |
'ultimate-post-kit', |
| 173 |
'ultimate-store-kit', |
| 174 |
'zoloblocks', |
| 175 |
'pixel-gallery', |
| 176 |
'live-copy-paste', |
| 177 |
'spin-wheel', |
| 178 |
'ai-image', |
| 179 |
'dark-reader', |
| 180 |
'ar-viewer', |
| 181 |
'smart-admin-assistant', |
| 182 |
'website-accessibility', |
| 183 |
]; |
| 184 |
|
| 185 |
$results = []; |
| 186 |
$errors = []; |
| 187 |
|
| 188 |
foreach ($plugin_slugs as $slug) { |
| 189 |
// Direct API fetch - no external dependencies |
| 190 |
$data = self::fetch_plugin_from_api($slug); |
| 191 |
if ($data !== false) { |
| 192 |
$results[$slug] = $data; |
| 193 |
} else { |
| 194 |
$errors[] = $slug; |
| 195 |
} |
| 196 |
} |
| 197 |
|
| 198 |
// Cache the results for 7 days |
| 199 |
set_transient(self::CACHE_KEY, $results, self::CACHE_DURATION); |
| 200 |
|
| 201 |
return $results; |
| 202 |
} |
| 203 |
|
| 204 |
/** |
| 205 |
* AJAX handler for getting plugins data |
| 206 |
*/ |
| 207 |
public static function ajax_get_plugins() { |
| 208 |
// Verify nonce for security |
| 209 |
if (!check_ajax_referer('upk_get_plugins_nonce', 'nonce', false)) { |
| 210 |
wp_send_json_error(['message' => __('Security check failed.', 'ultimate-post-kit')], 403); |
| 211 |
} |
| 212 |
|
| 213 |
// Gate to users who could act on it; also prevents the synchronous |
| 214 |
// remote-fetch trigger below from being reachable without capability. |
| 215 |
if (!current_user_can('install_plugins')) { |
| 216 |
wp_send_json_error(['message' => __('You do not have permission to do this.', 'ultimate-post-kit')], 403); |
| 217 |
} |
| 218 |
|
| 219 |
// Get cached data |
| 220 |
$plugins_data = self::get_remote_plugins(); |
| 221 |
|
| 222 |
// If cache is empty, fetch immediately for better UX |
| 223 |
if (empty($plugins_data)) { |
| 224 |
// Try to fetch data immediately (this is an AJAX request, so it's async already) |
| 225 |
$plugins_data = self::fetch_remote_plugins_now(); |
| 226 |
|
| 227 |
// If still empty after fetch, schedule background cron for retry |
| 228 |
if (empty($plugins_data)) { |
| 229 |
self::schedule_remote_fetch(); |
| 230 |
|
| 231 |
// Return empty response with flag indicating data is loading |
| 232 |
wp_send_json_success([ |
| 233 |
'plugins' => [], |
| 234 |
'loading' => true, |
| 235 |
'message' => __('Loading plugin data...', 'ultimate-post-kit') |
| 236 |
]); |
| 237 |
} |
| 238 |
} |
| 239 |
|
| 240 |
// Get recommended flags from Plugin_Integration_Helper (key may be 'slug' or 'slug/script.php') |
| 241 |
$recommended_by_slug = []; |
| 242 |
$helper_file = __DIR__ . '/class-plugin-integration-helper.php'; |
| 243 |
if (file_exists($helper_file)) { |
| 244 |
require_once $helper_file; |
| 245 |
$predefined = \UltimatePostKit\SetupWizard\Plugin_Integration_Helper::get_predefined_plugins(); |
| 246 |
foreach ($predefined as $key => $config) { |
| 247 |
$dir = (strpos($key, '/') !== false) ? dirname($key) : $key; |
| 248 |
$recommended_by_slug[$dir] = !empty($config['recommended']); |
| 249 |
} |
| 250 |
} |
| 251 |
|
| 252 |
// Format the response for frontend use |
| 253 |
$formatted_plugins = []; |
| 254 |
foreach ($plugins_data as $slug => $data) { |
| 255 |
// Check plugin status |
| 256 |
$plugin_status = self::get_plugin_status_by_slug($slug); |
| 257 |
$plugin_file = self::get_plugin_file_by_slug($slug); |
| 258 |
|
| 259 |
// Format the last updated date |
| 260 |
$last_updated_formatted = ''; |
| 261 |
if (!empty($data['last_updated'])) { |
| 262 |
$last_updated_formatted = self::format_last_updated($data['last_updated']); |
| 263 |
} |
| 264 |
|
| 265 |
$formatted_plugins[] = [ |
| 266 |
'name' => self::decode_api_text($data['name'] ?? ''), |
| 267 |
'slug' => $data['slug'] ?? '', |
| 268 |
'description' => self::decode_api_text($data['description'] ?? ''), |
| 269 |
'logo' => $data['logo'] ?? '', |
| 270 |
'rating' => $data['rating'] ?? 0, |
| 271 |
'rating_percentage' => $data['rating_percentage'] ?? 0, |
| 272 |
'num_ratings' => $data['num_ratings'] ?? 0, |
| 273 |
'active_installs' => $data['active_installs'] ?? '0', |
| 274 |
'active_installs_count' => $data['active_installs_count'] ?? 0, |
| 275 |
'downloaded' => $data['downloaded'] ?? 0, |
| 276 |
'downloaded_formatted' => $data['downloaded_formatted'] ?? '', |
| 277 |
'version' => $data['version'] ?? '', |
| 278 |
'tested' => $data['tested'] ?? '', |
| 279 |
'last_updated' => $data['last_updated'] ?? '', |
| 280 |
'last_updated_formatted' => $last_updated_formatted, |
| 281 |
'homepage' => $data['homepage'] ?? '', |
| 282 |
'status' => $plugin_status, |
| 283 |
'plugin_file' => $plugin_file, |
| 284 |
'activate_nonce' => $plugin_file ? wp_create_nonce('activate-plugin_' . $plugin_file) : '', |
| 285 |
'recommended' => !empty($recommended_by_slug[$slug]) |
| 286 |
]; |
| 287 |
} |
| 288 |
|
| 289 |
wp_send_json_success([ |
| 290 |
'plugins' => $formatted_plugins, |
| 291 |
'loading' => false, |
| 292 |
'message' => __('Plugin data loaded successfully.', 'ultimate-post-kit') |
| 293 |
]); |
| 294 |
} |
| 295 |
|
| 296 |
/** |
| 297 |
* Decode display text coming from the WordPress.org plugins API. |
| 298 |
* |
| 299 |
* The API returns strings that are already HTML-encoded, e.g. |
| 300 |
* "Element Pack Lite – Addons for Elementor". The renderer escapes |
| 301 |
* again before injecting into the DOM, which turns the leading "&" into |
| 302 |
* "&" and prints the entity literally instead of an en dash. Decoding |
| 303 |
* here means exactly one round of escaping happens, at output. |
| 304 |
* |
| 305 |
* Applied when building the response rather than when caching, so |
| 306 |
* already-cached entries are corrected without waiting for the transient |
| 307 |
* to expire. |
| 308 |
* |
| 309 |
* @param mixed $text Raw value from the API. |
| 310 |
* @return string Plain text, still to be escaped at output. |
| 311 |
*/ |
| 312 |
private static function decode_api_text($text) { |
| 313 |
if (!is_string($text) || '' === $text) { |
| 314 |
return ''; |
| 315 |
} |
| 316 |
|
| 317 |
return html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); |
| 318 |
} |
| 319 |
|
| 320 |
/** |
| 321 |
* Schedule the cron job on init |
| 322 |
*/ |
| 323 |
public static function schedule_cron() { |
| 324 |
// Make sure the cron hook is registered |
| 325 |
if (!wp_next_scheduled(self::CRON_HOOK)) { |
| 326 |
// Don't schedule immediately, only when needed |
| 327 |
} |
| 328 |
} |
| 329 |
|
| 330 |
/** |
| 331 |
* Get plugin status by slug |
| 332 |
* |
| 333 |
* @param string $slug Plugin slug |
| 334 |
* @return string Plugin status: 'active', 'installed', 'not_installed' |
| 335 |
*/ |
| 336 |
private static function get_plugin_status_by_slug($slug) { |
| 337 |
if (!function_exists('is_plugin_active')) { |
| 338 |
require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 339 |
} |
| 340 |
|
| 341 |
// Get all installed plugins |
| 342 |
$installed_plugins = get_plugins(); |
| 343 |
|
| 344 |
// Find the plugin file for this slug |
| 345 |
$plugin_file = self::get_plugin_file_by_slug($slug); |
| 346 |
|
| 347 |
if ($plugin_file && is_plugin_active($plugin_file)) { |
| 348 |
return 'active'; |
| 349 |
} elseif ($plugin_file && isset($installed_plugins[$plugin_file])) { |
| 350 |
return 'installed'; |
| 351 |
} |
| 352 |
|
| 353 |
return 'not_installed'; |
| 354 |
} |
| 355 |
|
| 356 |
/** |
| 357 |
* Get plugin file path by slug |
| 358 |
* |
| 359 |
* @param string $slug Plugin slug |
| 360 |
* @return string|null Plugin file path or null if not found |
| 361 |
*/ |
| 362 |
private static function get_plugin_file_by_slug($slug) { |
| 363 |
if (!function_exists('get_plugins')) { |
| 364 |
require_once ABSPATH . 'wp-admin/includes/plugin.php'; |
| 365 |
} |
| 366 |
|
| 367 |
$installed_plugins = get_plugins(); |
| 368 |
|
| 369 |
// Look for the plugin file that matches the slug |
| 370 |
foreach ($installed_plugins as $plugin_file => $plugin_data) { |
| 371 |
$plugin_slug = dirname($plugin_file); |
| 372 |
if ($plugin_slug === $slug) { |
| 373 |
return $plugin_file; |
| 374 |
} |
| 375 |
} |
| 376 |
|
| 377 |
return null; |
| 378 |
} |
| 379 |
|
| 380 |
/** |
| 381 |
* Format date in human-readable format |
| 382 |
* |
| 383 |
* @param string $date_string Date string to format |
| 384 |
* @return string Formatted date string |
| 385 |
*/ |
| 386 |
private static function format_last_updated($date_string) { |
| 387 |
if (empty($date_string)) { |
| 388 |
return __('Unknown', 'ultimate-post-kit'); |
| 389 |
} |
| 390 |
|
| 391 |
$date = strtotime($date_string); |
| 392 |
if (!$date) { |
| 393 |
return __('Unknown', 'ultimate-post-kit'); |
| 394 |
} |
| 395 |
|
| 396 |
$diff = current_time('timestamp') - $date; |
| 397 |
|
| 398 |
if ($diff < 60) { |
| 399 |
return __('Just now', 'ultimate-post-kit'); |
| 400 |
} elseif ($diff < 3600) { |
| 401 |
$minutes = floor($diff / 60); |
| 402 |
/* translators: %d: number of minutes */ |
| 403 |
return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, 'ultimate-post-kit'), $minutes); |
| 404 |
} elseif ($diff < 86400) { |
| 405 |
$hours = floor($diff / 3600); |
| 406 |
/* translators: %d: number of hours */ |
| 407 |
return sprintf(_n('%d hour ago', '%d hours ago', $hours, 'ultimate-post-kit'), $hours); |
| 408 |
} elseif ($diff < 2592000) { // 30 days |
| 409 |
$days = floor($diff / 86400); |
| 410 |
/* translators: %d: number of days */ |
| 411 |
return sprintf(_n('%d day ago', '%d days ago', $days, 'ultimate-post-kit'), $days); |
| 412 |
} elseif ($diff < 31536000) { // 1 year |
| 413 |
$months = floor($diff / 2592000); |
| 414 |
/* translators: %d: number of months */ |
| 415 |
return sprintf(_n('%d month ago', '%d months ago', $months, 'ultimate-post-kit'), $months); |
| 416 |
} else { |
| 417 |
$years = floor($diff / 31536000); |
| 418 |
/* translators: %d: number of years */ |
| 419 |
return sprintf(_n('%d year ago', '%d years ago', $years, 'ultimate-post-kit'), $years); |
| 420 |
} |
| 421 |
} |
| 422 |
|
| 423 |
/** |
| 424 |
* Fetch plugin data from WordPress.org API |
| 425 |
* |
| 426 |
* @param string $plugin_slug Plugin slug |
| 427 |
* @return array|false Plugin data or false on failure |
| 428 |
*/ |
| 429 |
private static function fetch_plugin_from_api($plugin_slug) { |
| 430 |
$api_url = add_query_arg([ |
| 431 |
'action' => 'plugin_information', |
| 432 |
'request' => [ |
| 433 |
'slug' => $plugin_slug, |
| 434 |
'fields' => [ |
| 435 |
'icons' => true, |
| 436 |
'short_description' => true, |
| 437 |
'active_installs' => true, |
| 438 |
'rating' => true, |
| 439 |
'num_ratings' => true, |
| 440 |
'downloaded' => true, |
| 441 |
'last_updated' => true, |
| 442 |
'homepage' => true, |
| 443 |
'tested' => true, |
| 444 |
'requires' => true, |
| 445 |
'requires_php' => true, |
| 446 |
'sections' => false, |
| 447 |
'compatibility' => false, |
| 448 |
'banners' => false, |
| 449 |
'contributors' => false, |
| 450 |
'tags' => false, |
| 451 |
'reviews' => false, |
| 452 |
'versions' => false, |
| 453 |
'installation' => false, |
| 454 |
'faq' => false, |
| 455 |
'changelog' => false, |
| 456 |
'screenshots' => false, |
| 457 |
'donate_link' => false, |
| 458 |
] |
| 459 |
] |
| 460 |
], 'https://api.wordpress.org/plugins/info/1.2/'); |
| 461 |
|
| 462 |
// Security: Use wp_safe_remote_get instead of wp_remote_get |
| 463 |
$response = wp_safe_remote_get($api_url, [ |
| 464 |
'timeout' => 30, |
| 465 |
'user-agent' => 'Ultimate Post Kit Setup Wizard' |
| 466 |
]); |
| 467 |
|
| 468 |
if (is_wp_error($response)) { |
| 469 |
return false; |
| 470 |
} |
| 471 |
|
| 472 |
$body = wp_remote_retrieve_body($response); |
| 473 |
$data = json_decode($body, true); |
| 474 |
|
| 475 |
if (empty($data) || !is_array($data)) { |
| 476 |
return false; |
| 477 |
} |
| 478 |
|
| 479 |
$formatted_data = self::format_plugin_data($data); |
| 480 |
|
| 481 |
if (empty($formatted_data['name']) && empty($formatted_data['slug'])) { |
| 482 |
return false; |
| 483 |
} |
| 484 |
|
| 485 |
return $formatted_data; |
| 486 |
} |
| 487 |
|
| 488 |
/** |
| 489 |
* Format plugin data for our use |
| 490 |
* |
| 491 |
* @param array $raw_data Raw API data |
| 492 |
* @return array Formatted plugin data |
| 493 |
*/ |
| 494 |
private static function format_plugin_data($raw_data) { |
| 495 |
// Prefer the logo bundled with this plugin so the cards show our own branded |
| 496 |
// artwork; fall back to the wordpress.org icon for anything not bundled. |
| 497 |
$icon_url = self::get_local_plugin_logo($raw_data['slug'] ?? ''); |
| 498 |
|
| 499 |
if ('' === $icon_url) { |
| 500 |
$icon_url = self::get_valid_plugin_icon($raw_data['icons'] ?? []); |
| 501 |
} |
| 502 |
|
| 503 |
// Format active installs with null safety and real data |
| 504 |
$active_installs_raw = $raw_data['active_installs'] ?? 0; |
| 505 |
$active_installs = self::format_active_installs($active_installs_raw); |
| 506 |
$active_installs_count = self::get_numeric_active_installs($active_installs_raw); |
| 507 |
|
| 508 |
// Calculate rating percentage with null safety and real data |
| 509 |
$rating_percentage = 0; |
| 510 |
$rating_raw = $raw_data['rating'] ?? 0; |
| 511 |
$num_ratings_raw = $raw_data['num_ratings'] ?? 0; |
| 512 |
|
| 513 |
if (!empty($rating_raw) && !empty($num_ratings_raw)) { |
| 514 |
$rating_percentage = ($rating_raw / 100) * 5; // Convert to 5-star scale |
| 515 |
} |
| 516 |
|
| 517 |
// Get downloaded count for additional metrics |
| 518 |
$downloaded_count = $raw_data['downloaded'] ?? 0; |
| 519 |
|
| 520 |
return [ |
| 521 |
'name' => $raw_data['name'] ?? '', |
| 522 |
'slug' => $raw_data['slug'] ?? '', |
| 523 |
'logo' => $icon_url, |
| 524 |
'description' => $raw_data['short_description'] ?? '', |
| 525 |
'active_installs' => $active_installs, |
| 526 |
'active_installs_count' => $active_installs_count, |
| 527 |
'rating' => round($rating_percentage, 1), |
| 528 |
'rating_percentage' => $rating_raw, |
| 529 |
'num_ratings' => $num_ratings_raw, |
| 530 |
'downloaded' => $downloaded_count, |
| 531 |
'downloaded_formatted' => self::format_downloaded_count($downloaded_count), |
| 532 |
'last_updated' => $raw_data['last_updated'] ?? '', |
| 533 |
'homepage' => $raw_data['homepage'] ?? '', |
| 534 |
'version' => $raw_data['version'] ?? '', |
| 535 |
'tested' => $raw_data['tested'] ?? '', |
| 536 |
'requires' => $raw_data['requires'] ?? '', |
| 537 |
'requires_php' => $raw_data['requires_php'] ?? '', |
| 538 |
'fetched_at' => current_time('timestamp') |
| 539 |
]; |
| 540 |
} |
| 541 |
|
| 542 |
/** |
| 543 |
* Get valid plugin icon with format validation |
| 544 |
* |
| 545 |
* @param array $icons Array of icon URLs |
| 546 |
* @return string Valid icon URL or empty string |
| 547 |
*/ |
| 548 |
private static function get_valid_plugin_icon($icons) { |
| 549 |
$valid_extensions = ['gif', 'png', 'jpg', 'jpeg', 'svg']; |
| 550 |
|
| 551 |
// The wordpress.org plugin_information API returns its icon map keyed by |
| 552 |
// '2x' / '1x' (and 'svg' or 'default' for the generated geopattern icon) -- |
| 553 |
// never '256' / '128'. Looking only for the pixel keys meant no plugin icon |
| 554 |
// ever resolved and every card fell back to the placeholder. Highest quality |
| 555 |
// first, with the old pixel keys kept for any cached/legacy payload. |
| 556 |
$icon_sizes = ['2x', '1x', 'svg', 'default', '256', '128']; |
| 557 |
|
| 558 |
foreach ($icon_sizes as $size) { |
| 559 |
if (!empty($icons[$size])) { |
| 560 |
$icon_url = $icons[$size]; |
| 561 |
|
| 562 |
// Check if URL is valid and has correct extension |
| 563 |
if (self::is_valid_image_url($icon_url, $valid_extensions)) { |
| 564 |
return $icon_url; |
| 565 |
} |
| 566 |
} |
| 567 |
} |
| 568 |
|
| 569 |
return ''; |
| 570 |
} |
| 571 |
|
| 572 |
/** |
| 573 |
* Validate image URL and extension |
| 574 |
* |
| 575 |
* @param string $url Image URL |
| 576 |
* @param array $valid_extensions Allowed extensions |
| 577 |
* @return bool True if valid |
| 578 |
*/ |
| 579 |
private static function is_valid_image_url($url, $valid_extensions) { |
| 580 |
if (empty($url) || !is_string($url)) { |
| 581 |
return false; |
| 582 |
} |
| 583 |
|
| 584 |
// Check if URL is valid |
| 585 |
if (!filter_var($url, FILTER_VALIDATE_URL)) { |
| 586 |
return false; |
| 587 |
} |
| 588 |
|
| 589 |
// Get file extension |
| 590 |
$path_info = pathinfo(wp_parse_url($url, PHP_URL_PATH)); |
| 591 |
$extension = strtolower($path_info['extension'] ?? ''); |
| 592 |
|
| 593 |
return in_array($extension, $valid_extensions); |
| 594 |
} |
| 595 |
|
| 596 |
/** |
| 597 |
* Format active installs number with null safety |
| 598 |
* |
| 599 |
* @param mixed $installs Number of active installs |
| 600 |
* @return string Formatted installs string |
| 601 |
*/ |
| 602 |
private static function format_active_installs($installs) { |
| 603 |
// Handle null, empty, or non-numeric values |
| 604 |
if (is_null($installs) || $installs === '' || !is_numeric($installs)) { |
| 605 |
return '0'; |
| 606 |
} |
| 607 |
|
| 608 |
$installs = intval($installs); |
| 609 |
|
| 610 |
if ($installs >= 1000000) { |
| 611 |
return round($installs / 1000000, 1) . 'M+'; |
| 612 |
} elseif ($installs >= 1000) { |
| 613 |
return round($installs / 1000, 1) . 'K+'; |
| 614 |
} else { |
| 615 |
return number_format($installs); |
| 616 |
} |
| 617 |
} |
| 618 |
|
| 619 |
/** |
| 620 |
* Get numeric active installs count |
| 621 |
* |
| 622 |
* @param mixed $installs Number of active installs |
| 623 |
* @return int Numeric installs count |
| 624 |
*/ |
| 625 |
private static function get_numeric_active_installs($installs) { |
| 626 |
// Handle null, empty, or non-numeric values |
| 627 |
if (is_null($installs) || $installs === '' || !is_numeric($installs)) { |
| 628 |
return 0; |
| 629 |
} |
| 630 |
|
| 631 |
return intval($installs); |
| 632 |
} |
| 633 |
|
| 634 |
/** |
| 635 |
* Format downloaded count |
| 636 |
* |
| 637 |
* @param mixed $downloaded Number of downloads |
| 638 |
* @return string Formatted downloads string |
| 639 |
*/ |
| 640 |
private static function format_downloaded_count($downloaded) { |
| 641 |
// Handle null, empty, or non-numeric values |
| 642 |
if (is_null($downloaded) || $downloaded === '' || !is_numeric($downloaded)) { |
| 643 |
return '0'; |
| 644 |
} |
| 645 |
|
| 646 |
$downloaded = intval($downloaded); |
| 647 |
|
| 648 |
if ($downloaded >= 1000000) { |
| 649 |
return round($downloaded / 1000000, 1) . 'M+'; |
| 650 |
} elseif ($downloaded >= 1000) { |
| 651 |
return round($downloaded / 1000, 1) . 'K+'; |
| 652 |
} else { |
| 653 |
return number_format($downloaded); |
| 654 |
} |
| 655 |
} |
| 656 |
} |
| 657 |
|
| 658 |
// Initialize the handler |
| 659 |
add_action('init', function() { |
| 660 |
Remote_Data_Handler::init(); |
| 661 |
}); |
| 662 |
|
| 663 |
// Global functions for backward compatibility and ease of use |
| 664 |
if (!function_exists('upk_is_element_pack_page')) { |
| 665 |
function upk_is_element_pack_page() { |
| 666 |
return \UltimatePostKit\SetupWizard\Remote_Data_Handler::is_element_pack_page(); |
| 667 |
} |
| 668 |
} |
| 669 |
|
| 670 |
if (!function_exists('upk_get_remote_plugins')) { |
| 671 |
function upk_get_remote_plugins() { |
| 672 |
return \UltimatePostKit\SetupWizard\Remote_Data_Handler::get_remote_plugins(); |
| 673 |
} |
| 674 |
} |
| 675 |
|
| 676 |
if (!function_exists('upk_schedule_remote_fetch')) { |
| 677 |
function upk_schedule_remote_fetch() { |
| 678 |
return \UltimatePostKit\SetupWizard\Remote_Data_Handler::schedule_remote_fetch(); |
| 679 |
} |
| 680 |
} |
| 681 |
|
| 682 |
if (!function_exists('upk_fetch_remote_plugins_now')) { |
| 683 |
function upk_fetch_remote_plugins_now() { |
| 684 |
return \UltimatePostKit\SetupWizard\Remote_Data_Handler::fetch_remote_plugins_now(); |
| 685 |
} |
| 686 |
} |
| 687 |
|