PluginProbe
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets / 4.5.6
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets v4.5.6
4.5.6 4.5.5 4.5.4 4.2.1 4.2.2 4.2.3 4.5.0 4.5.2 4.5.3 4.2.0 4.1.18 4.1.17 4.1.16 4.1.15 4.1.14 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.0.9 4.1.0 4.1.1 4.1.2 All 148 releases
← All changes | includes/setup-wizard/class-remote-data-handler.php +108 -11 4.2.0 → 4.5.6 View file →
@@ -26,8 +26,61 @@
26 26 */
27 27 const CACHE_KEY = 'bdt_remote_plugins_data';
28 28
29 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 + /**
30 83 * Cron hook name for background fetch
31 84 */
32 85 const CRON_HOOK = 'bdt_fetch_remote_plugins_cron';
33 86
@@ -36,10 +89,10 @@
36 89 */
37 90 public static function init() {
38 91 add_action('init', [__CLASS__, 'schedule_cron']);
39 92 add_action(self::CRON_HOOK, [__CLASS__, 'cron_fetch_plugins']);
93 + // Admin-only plugin-install data; never expose to unauthenticated visitors.
40 94 add_action('wp_ajax_upk_get_plugins', [__CLASS__, 'ajax_get_plugins']);
41 - add_action('wp_ajax_nopriv_upk_get_plugins', [__CLASS__, 'ajax_get_plugins']);
42 95 }
43 96
44 97 /**
45 98 * WP-Cron callback for fetching plugins
@@ -58,17 +111,20 @@
58 111 return false;
59 112 }
60 113
61 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.
62 116 if (wp_doing_ajax() && isset($_REQUEST['action'])) {
63 - $action = sanitize_text_field($_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']));
64 119 if (in_array($action, ['upk_get_plugins'])) {
65 120 return true;
66 121 }
67 122 }
68 123
69 - $page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : '';
70 - return $page === 'element_pack_options';
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';
71 127 }
72 128
73 129 /**
74 130 * Get remote plugins data from cache
@@ -150,11 +206,17 @@
150 206 */
151 207 public static function ajax_get_plugins() {
152 208 // Verify nonce for security
153 209 if (!check_ajax_referer('upk_get_plugins_nonce', 'nonce', false)) {
154 - wp_die(esc_html__('Security check failed.', 'ultimate-post-kit'));
210 + wp_send_json_error(['message' => __('Security check failed.', 'ultimate-post-kit')], 403);
155 211 }
156 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 +
157 219 // Get cached data
158 220 $plugins_data = self::get_remote_plugins();
159 221
160 222 // If cache is empty, fetch immediately for better UX
@@ -200,11 +262,11 @@
200 262 $last_updated_formatted = self::format_last_updated($data['last_updated']);
201 263 }
202 264
203 265 $formatted_plugins[] = [
204 - 'name' => $data['name'] ?? '',
266 + 'name' => self::decode_api_text($data['name'] ?? ''),
205 267 'slug' => $data['slug'] ?? '',
206 - 'description' => $data['description'] ?? '',
268 + 'description' => self::decode_api_text($data['description'] ?? ''),
207 269 'logo' => $data['logo'] ?? '',
208 270 'rating' => $data['rating'] ?? 0,
209 271 'rating_percentage' => $data['rating_percentage'] ?? 0,
210 272 'num_ratings' => $data['num_ratings'] ?? 0,
@@ -231,8 +293,32 @@
231 293 ]);
232 294 }
233 295
234 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 &#8211; Addons for Elementor". The renderer escapes
301 + * again before injecting into the DOM, which turns the leading "&" into
302 + * "&amp;" 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 + /**
235 321 * Schedule the cron job on init
236 322 */
237 323 public static function schedule_cron() {
238 324 // Make sure the cron hook is registered
@@ -405,11 +491,16 @@
405 491 * @param array $raw_data Raw API data
406 492 * @return array Formatted plugin data
407 493 */
408 494 private static function format_plugin_data($raw_data) {
409 - // Get the best available icon with validation
410 - $icon_url = self::get_valid_plugin_icon($raw_data['icons'] ?? []);
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'] ?? '');
411 498
499 + if ('' === $icon_url) {
500 + $icon_url = self::get_valid_plugin_icon($raw_data['icons'] ?? []);
501 + }
502 +
412 503 // Format active installs with null safety and real data
413 504 $active_installs_raw = $raw_data['active_installs'] ?? 0;
414 505 $active_installs = self::format_active_installs($active_installs_raw);
415 506 $active_installs_count = self::get_numeric_active_installs($active_installs_raw);
@@ -455,10 +546,16 @@
455 546 * @return string Valid icon URL or empty string
456 547 */
457 548 private static function get_valid_plugin_icon($icons) {
458 549 $valid_extensions = ['gif', 'png', 'jpg', 'jpeg', 'svg'];
459 - $icon_sizes = ['256', '128', 'default'];
460 -
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 +
461 558 foreach ($icon_sizes as $size) {
462 559 if (!empty($icons[$size])) {
463 560 $icon_url = $icons[$size];
464 561