*/ const LOCAL_PLUGIN_LOGOS = [ 'bdthemes-element-pack-lite' => 'element-pack', 'bdthemes-prime-slider-lite' => 'prime-slider', 'ultimate-post-kit' => 'ultimate-post-kit', 'ultimate-store-kit' => 'ultimate-store-kit', 'zoloblocks' => 'zoloblocks', 'pixel-gallery' => 'pixel-gallery', 'live-copy-paste' => 'live-copy-paste', 'spin-wheel' => 'spin-wheel', 'ai-image' => 'ai-image', 'dark-reader' => 'dark-reader', 'ar-viewer' => 'ar-viewer', 'smart-admin-assistant' => 'smart-admin-assistant', 'website-accessibility' => 'one-accessibility', 'launch-guard' => 'launch-guard', 'sigma-forms' => 'sigma-forms', 'sigma-media-manager' => 'sigma-media-manager', 'sigma-store-locator' => 'sigma-store-locator', 'swift-checkout' => 'swift-checkout', ]; /** * Resolve the bundled logo URL for a plugin slug. * * @param string $slug Plugin slug. * @return string Logo URL, or an empty string when the slug has no bundled logo. */ public static function get_local_plugin_logo( $slug ) { if ( ! is_string( $slug ) || '' === $slug || ! isset( self::LOCAL_PLUGIN_LOGOS[ $slug ] ) ) { return ''; } $file = self::LOCAL_PLUGIN_LOGOS[ $slug ] . '.png'; // Only advertise the file if it actually shipped, so a trimmed build falls // back to the remote icon rather than rendering a broken image. if ( defined( 'BDTUPK_PATH' ) && ! file_exists( BDTUPK_PATH . 'assets/images/others-plugin-logo/' . $file ) ) { return ''; } return BDTUPK_ASSETS_URL . 'images/others-plugin-logo/' . $file; } /** * Cron hook name for background fetch */ const CRON_HOOK = 'bdt_fetch_remote_plugins_cron'; /** * Initialize the remote data handler */ public static function init() { add_action('init', [__CLASS__, 'schedule_cron']); add_action(self::CRON_HOOK, [__CLASS__, 'cron_fetch_plugins']); // Admin-only plugin-install data; never expose to unauthenticated visitors. add_action('wp_ajax_upk_get_plugins', [__CLASS__, 'ajax_get_plugins']); } /** * WP-Cron callback for fetching plugins */ public static function cron_fetch_plugins() { self::fetch_remote_plugins_now(); } /** * Check if we're on the Ultimate Post Kit options page * * @return bool True if on Ultimate Post Kit options page */ public static function is_element_pack_page() { if (!is_admin()) { return false; } // Check if this is an AJAX request for our plugins // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check of the AJAX action name, no form data processed. if (wp_doing_ajax() && isset($_REQUEST['action'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check of the AJAX action name, no form data processed. $action = sanitize_text_field(wp_unslash($_REQUEST['action'])); if (in_array($action, ['upk_get_plugins'])) { return true; } } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only check of the current admin page slug, no form data processed. $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; return $page === 'ultimate_post_kit_options'; } /** * Get remote plugins data from cache * * @return array Cached plugins data or empty array if not available */ public static function get_remote_plugins() { $cached_data = get_transient(self::CACHE_KEY); if ($cached_data !== false) { return $cached_data; } // If no cache exists, schedule background fetch and return empty array self::schedule_remote_fetch(); return []; } /** * Schedule a background fetch via WP-Cron * * @return bool True if successfully scheduled */ public static function schedule_remote_fetch() { // Schedule to run immediately if not already scheduled if (!wp_next_scheduled(self::CRON_HOOK)) { wp_schedule_single_event(time(), self::CRON_HOOK); return true; } return false; } /** * Fetch remote plugins data immediately (for background processing only) * * @return array|false Plugins data or false on failure */ public static function fetch_remote_plugins_now() { // Define plugin slugs to fetch $plugin_slugs = [ 'bdthemes-element-pack-lite', 'bdthemes-prime-slider-lite', 'ultimate-post-kit', 'ultimate-store-kit', 'zoloblocks', 'pixel-gallery', 'live-copy-paste', 'spin-wheel', 'ai-image', 'dark-reader', 'ar-viewer', 'smart-admin-assistant', 'website-accessibility', ]; $results = []; $errors = []; foreach ($plugin_slugs as $slug) { // Direct API fetch - no external dependencies $data = self::fetch_plugin_from_api($slug); if ($data !== false) { $results[$slug] = $data; } else { $errors[] = $slug; } } // Cache the results for 7 days set_transient(self::CACHE_KEY, $results, self::CACHE_DURATION); return $results; } /** * AJAX handler for getting plugins data */ public static function ajax_get_plugins() { // Verify nonce for security if (!check_ajax_referer('upk_get_plugins_nonce', 'nonce', false)) { wp_send_json_error(['message' => __('Security check failed.', 'ultimate-post-kit')], 403); } // Gate to users who could act on it; also prevents the synchronous // remote-fetch trigger below from being reachable without capability. if (!current_user_can('install_plugins')) { wp_send_json_error(['message' => __('You do not have permission to do this.', 'ultimate-post-kit')], 403); } // Get cached data $plugins_data = self::get_remote_plugins(); // If cache is empty, fetch immediately for better UX if (empty($plugins_data)) { // Try to fetch data immediately (this is an AJAX request, so it's async already) $plugins_data = self::fetch_remote_plugins_now(); // If still empty after fetch, schedule background cron for retry if (empty($plugins_data)) { self::schedule_remote_fetch(); // Return empty response with flag indicating data is loading wp_send_json_success([ 'plugins' => [], 'loading' => true, 'message' => __('Loading plugin data...', 'ultimate-post-kit') ]); } } // Get recommended flags from Plugin_Integration_Helper (key may be 'slug' or 'slug/script.php') $recommended_by_slug = []; $helper_file = __DIR__ . '/class-plugin-integration-helper.php'; if (file_exists($helper_file)) { require_once $helper_file; $predefined = \UltimatePostKit\SetupWizard\Plugin_Integration_Helper::get_predefined_plugins(); foreach ($predefined as $key => $config) { $dir = (strpos($key, '/') !== false) ? dirname($key) : $key; $recommended_by_slug[$dir] = !empty($config['recommended']); } } // Format the response for frontend use $formatted_plugins = []; foreach ($plugins_data as $slug => $data) { // Check plugin status $plugin_status = self::get_plugin_status_by_slug($slug); $plugin_file = self::get_plugin_file_by_slug($slug); // Format the last updated date $last_updated_formatted = ''; if (!empty($data['last_updated'])) { $last_updated_formatted = self::format_last_updated($data['last_updated']); } $formatted_plugins[] = [ 'name' => self::decode_api_text($data['name'] ?? ''), 'slug' => $data['slug'] ?? '', 'description' => self::decode_api_text($data['description'] ?? ''), 'logo' => $data['logo'] ?? '', 'rating' => $data['rating'] ?? 0, 'rating_percentage' => $data['rating_percentage'] ?? 0, 'num_ratings' => $data['num_ratings'] ?? 0, 'active_installs' => $data['active_installs'] ?? '0', 'active_installs_count' => $data['active_installs_count'] ?? 0, 'downloaded' => $data['downloaded'] ?? 0, 'downloaded_formatted' => $data['downloaded_formatted'] ?? '', 'version' => $data['version'] ?? '', 'tested' => $data['tested'] ?? '', 'last_updated' => $data['last_updated'] ?? '', 'last_updated_formatted' => $last_updated_formatted, 'homepage' => $data['homepage'] ?? '', 'status' => $plugin_status, 'plugin_file' => $plugin_file, 'activate_nonce' => $plugin_file ? wp_create_nonce('activate-plugin_' . $plugin_file) : '', 'recommended' => !empty($recommended_by_slug[$slug]) ]; } wp_send_json_success([ 'plugins' => $formatted_plugins, 'loading' => false, 'message' => __('Plugin data loaded successfully.', 'ultimate-post-kit') ]); } /** * Decode display text coming from the WordPress.org plugins API. * * The API returns strings that are already HTML-encoded, e.g. * "Element Pack Lite – Addons for Elementor". The renderer escapes * again before injecting into the DOM, which turns the leading "&" into * "&" and prints the entity literally instead of an en dash. Decoding * here means exactly one round of escaping happens, at output. * * Applied when building the response rather than when caching, so * already-cached entries are corrected without waiting for the transient * to expire. * * @param mixed $text Raw value from the API. * @return string Plain text, still to be escaped at output. */ private static function decode_api_text($text) { if (!is_string($text) || '' === $text) { return ''; } return html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } /** * Schedule the cron job on init */ public static function schedule_cron() { // Make sure the cron hook is registered if (!wp_next_scheduled(self::CRON_HOOK)) { // Don't schedule immediately, only when needed } } /** * Get plugin status by slug * * @param string $slug Plugin slug * @return string Plugin status: 'active', 'installed', 'not_installed' */ private static function get_plugin_status_by_slug($slug) { if (!function_exists('is_plugin_active')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } // Get all installed plugins $installed_plugins = get_plugins(); // Find the plugin file for this slug $plugin_file = self::get_plugin_file_by_slug($slug); if ($plugin_file && is_plugin_active($plugin_file)) { return 'active'; } elseif ($plugin_file && isset($installed_plugins[$plugin_file])) { return 'installed'; } return 'not_installed'; } /** * Get plugin file path by slug * * @param string $slug Plugin slug * @return string|null Plugin file path or null if not found */ private static function get_plugin_file_by_slug($slug) { if (!function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $installed_plugins = get_plugins(); // Look for the plugin file that matches the slug foreach ($installed_plugins as $plugin_file => $plugin_data) { $plugin_slug = dirname($plugin_file); if ($plugin_slug === $slug) { return $plugin_file; } } return null; } /** * Format date in human-readable format * * @param string $date_string Date string to format * @return string Formatted date string */ private static function format_last_updated($date_string) { if (empty($date_string)) { return __('Unknown', 'ultimate-post-kit'); } $date = strtotime($date_string); if (!$date) { return __('Unknown', 'ultimate-post-kit'); } $diff = current_time('timestamp') - $date; if ($diff < 60) { return __('Just now', 'ultimate-post-kit'); } elseif ($diff < 3600) { $minutes = floor($diff / 60); /* translators: %d: number of minutes */ return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, 'ultimate-post-kit'), $minutes); } elseif ($diff < 86400) { $hours = floor($diff / 3600); /* translators: %d: number of hours */ return sprintf(_n('%d hour ago', '%d hours ago', $hours, 'ultimate-post-kit'), $hours); } elseif ($diff < 2592000) { // 30 days $days = floor($diff / 86400); /* translators: %d: number of days */ return sprintf(_n('%d day ago', '%d days ago', $days, 'ultimate-post-kit'), $days); } elseif ($diff < 31536000) { // 1 year $months = floor($diff / 2592000); /* translators: %d: number of months */ return sprintf(_n('%d month ago', '%d months ago', $months, 'ultimate-post-kit'), $months); } else { $years = floor($diff / 31536000); /* translators: %d: number of years */ return sprintf(_n('%d year ago', '%d years ago', $years, 'ultimate-post-kit'), $years); } } /** * Fetch plugin data from WordPress.org API * * @param string $plugin_slug Plugin slug * @return array|false Plugin data or false on failure */ private static function fetch_plugin_from_api($plugin_slug) { $api_url = add_query_arg([ 'action' => 'plugin_information', 'request' => [ 'slug' => $plugin_slug, 'fields' => [ 'icons' => true, 'short_description' => true, 'active_installs' => true, 'rating' => true, 'num_ratings' => true, 'downloaded' => true, 'last_updated' => true, 'homepage' => true, 'tested' => true, 'requires' => true, 'requires_php' => true, 'sections' => false, 'compatibility' => false, 'banners' => false, 'contributors' => false, 'tags' => false, 'reviews' => false, 'versions' => false, 'installation' => false, 'faq' => false, 'changelog' => false, 'screenshots' => false, 'donate_link' => false, ] ] ], 'https://api.wordpress.org/plugins/info/1.2/'); // Security: Use wp_safe_remote_get instead of wp_remote_get $response = wp_safe_remote_get($api_url, [ 'timeout' => 30, 'user-agent' => 'Ultimate Post Kit Setup Wizard' ]); if (is_wp_error($response)) { return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (empty($data) || !is_array($data)) { return false; } $formatted_data = self::format_plugin_data($data); if (empty($formatted_data['name']) && empty($formatted_data['slug'])) { return false; } return $formatted_data; } /** * Format plugin data for our use * * @param array $raw_data Raw API data * @return array Formatted plugin data */ private static function format_plugin_data($raw_data) { // Prefer the logo bundled with this plugin so the cards show our own branded // artwork; fall back to the wordpress.org icon for anything not bundled. $icon_url = self::get_local_plugin_logo($raw_data['slug'] ?? ''); if ('' === $icon_url) { $icon_url = self::get_valid_plugin_icon($raw_data['icons'] ?? []); } // Format active installs with null safety and real data $active_installs_raw = $raw_data['active_installs'] ?? 0; $active_installs = self::format_active_installs($active_installs_raw); $active_installs_count = self::get_numeric_active_installs($active_installs_raw); // Calculate rating percentage with null safety and real data $rating_percentage = 0; $rating_raw = $raw_data['rating'] ?? 0; $num_ratings_raw = $raw_data['num_ratings'] ?? 0; if (!empty($rating_raw) && !empty($num_ratings_raw)) { $rating_percentage = ($rating_raw / 100) * 5; // Convert to 5-star scale } // Get downloaded count for additional metrics $downloaded_count = $raw_data['downloaded'] ?? 0; return [ 'name' => $raw_data['name'] ?? '', 'slug' => $raw_data['slug'] ?? '', 'logo' => $icon_url, 'description' => $raw_data['short_description'] ?? '', 'active_installs' => $active_installs, 'active_installs_count' => $active_installs_count, 'rating' => round($rating_percentage, 1), 'rating_percentage' => $rating_raw, 'num_ratings' => $num_ratings_raw, 'downloaded' => $downloaded_count, 'downloaded_formatted' => self::format_downloaded_count($downloaded_count), 'last_updated' => $raw_data['last_updated'] ?? '', 'homepage' => $raw_data['homepage'] ?? '', 'version' => $raw_data['version'] ?? '', 'tested' => $raw_data['tested'] ?? '', 'requires' => $raw_data['requires'] ?? '', 'requires_php' => $raw_data['requires_php'] ?? '', 'fetched_at' => current_time('timestamp') ]; } /** * Get valid plugin icon with format validation * * @param array $icons Array of icon URLs * @return string Valid icon URL or empty string */ private static function get_valid_plugin_icon($icons) { $valid_extensions = ['gif', 'png', 'jpg', 'jpeg', 'svg']; // The wordpress.org plugin_information API returns its icon map keyed by // '2x' / '1x' (and 'svg' or 'default' for the generated geopattern icon) -- // never '256' / '128'. Looking only for the pixel keys meant no plugin icon // ever resolved and every card fell back to the placeholder. Highest quality // first, with the old pixel keys kept for any cached/legacy payload. $icon_sizes = ['2x', '1x', 'svg', 'default', '256', '128']; foreach ($icon_sizes as $size) { if (!empty($icons[$size])) { $icon_url = $icons[$size]; // Check if URL is valid and has correct extension if (self::is_valid_image_url($icon_url, $valid_extensions)) { return $icon_url; } } } return ''; } /** * Validate image URL and extension * * @param string $url Image URL * @param array $valid_extensions Allowed extensions * @return bool True if valid */ private static function is_valid_image_url($url, $valid_extensions) { if (empty($url) || !is_string($url)) { return false; } // Check if URL is valid if (!filter_var($url, FILTER_VALIDATE_URL)) { return false; } // Get file extension $path_info = pathinfo(wp_parse_url($url, PHP_URL_PATH)); $extension = strtolower($path_info['extension'] ?? ''); return in_array($extension, $valid_extensions); } /** * Format active installs number with null safety * * @param mixed $installs Number of active installs * @return string Formatted installs string */ private static function format_active_installs($installs) { // Handle null, empty, or non-numeric values if (is_null($installs) || $installs === '' || !is_numeric($installs)) { return '0'; } $installs = intval($installs); if ($installs >= 1000000) { return round($installs / 1000000, 1) . 'M+'; } elseif ($installs >= 1000) { return round($installs / 1000, 1) . 'K+'; } else { return number_format($installs); } } /** * Get numeric active installs count * * @param mixed $installs Number of active installs * @return int Numeric installs count */ private static function get_numeric_active_installs($installs) { // Handle null, empty, or non-numeric values if (is_null($installs) || $installs === '' || !is_numeric($installs)) { return 0; } return intval($installs); } /** * Format downloaded count * * @param mixed $downloaded Number of downloads * @return string Formatted downloads string */ private static function format_downloaded_count($downloaded) { // Handle null, empty, or non-numeric values if (is_null($downloaded) || $downloaded === '' || !is_numeric($downloaded)) { return '0'; } $downloaded = intval($downloaded); if ($downloaded >= 1000000) { return round($downloaded / 1000000, 1) . 'M+'; } elseif ($downloaded >= 1000) { return round($downloaded / 1000, 1) . 'K+'; } else { return number_format($downloaded); } } } // Initialize the handler add_action('init', function() { Remote_Data_Handler::init(); }); // Global functions for backward compatibility and ease of use if (!function_exists('upk_is_element_pack_page')) { function upk_is_element_pack_page() { return \UltimatePostKit\SetupWizard\Remote_Data_Handler::is_element_pack_page(); } } if (!function_exists('upk_get_remote_plugins')) { function upk_get_remote_plugins() { return \UltimatePostKit\SetupWizard\Remote_Data_Handler::get_remote_plugins(); } } if (!function_exists('upk_schedule_remote_fetch')) { function upk_schedule_remote_fetch() { return \UltimatePostKit\SetupWizard\Remote_Data_Handler::schedule_remote_fetch(); } } if (!function_exists('upk_fetch_remote_plugins_now')) { function upk_fetch_remote_plugins_now() { return \UltimatePostKit\SetupWizard\Remote_Data_Handler::fetch_remote_plugins_now(); } }