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 +114 -12 4.1.16 → 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(__('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
@@ -312,20 +398,25 @@
312 398 if ($diff < 60) {
313 399 return __('Just now', 'ultimate-post-kit');
314 400 } elseif ($diff < 3600) {
315 401 $minutes = floor($diff / 60);
402 + /* translators: %d: number of minutes */
316 403 return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, 'ultimate-post-kit'), $minutes);
317 404 } elseif ($diff < 86400) {
318 405 $hours = floor($diff / 3600);
406 + /* translators: %d: number of hours */
319 407 return sprintf(_n('%d hour ago', '%d hours ago', $hours, 'ultimate-post-kit'), $hours);
320 408 } elseif ($diff < 2592000) { // 30 days
321 409 $days = floor($diff / 86400);
410 + /* translators: %d: number of days */
322 411 return sprintf(_n('%d day ago', '%d days ago', $days, 'ultimate-post-kit'), $days);
323 412 } elseif ($diff < 31536000) { // 1 year
324 413 $months = floor($diff / 2592000);
414 + /* translators: %d: number of months */
325 415 return sprintf(_n('%d month ago', '%d months ago', $months, 'ultimate-post-kit'), $months);
326 416 } else {
327 417 $years = floor($diff / 31536000);
418 + /* translators: %d: number of years */
328 419 return sprintf(_n('%d year ago', '%d years ago', $years, 'ultimate-post-kit'), $years);
329 420 }
330 421 }
331 422
@@ -400,11 +491,16 @@
400 491 * @param array $raw_data Raw API data
401 492 * @return array Formatted plugin data
402 493 */
403 494 private static function format_plugin_data($raw_data) {
404 - // Get the best available icon with validation
405 - $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'] ?? '');
406 498
499 + if ('' === $icon_url) {
500 + $icon_url = self::get_valid_plugin_icon($raw_data['icons'] ?? []);
501 + }
502 +
407 503 // Format active installs with null safety and real data
408 504 $active_installs_raw = $raw_data['active_installs'] ?? 0;
409 505 $active_installs = self::format_active_installs($active_installs_raw);
410 506 $active_installs_count = self::get_numeric_active_installs($active_installs_raw);
@@ -450,10 +546,16 @@
450 546 * @return string Valid icon URL or empty string
451 547 */
452 548 private static function get_valid_plugin_icon($icons) {
453 549 $valid_extensions = ['gif', 'png', 'jpg', 'jpeg', 'svg'];
454 - $icon_sizes = ['256', '128', 'default'];
455 -
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 +
456 558 foreach ($icon_sizes as $size) {
457 559 if (!empty($icons[$size])) {
458 560 $icon_url = $icons[$size];
459 561
@@ -484,9 +586,9 @@
484 586 return false;
485 587 }
486 588
487 589 // Get file extension
488 - $path_info = pathinfo(parse_url($url, PHP_URL_PATH));
590 + $path_info = pathinfo(wp_parse_url($url, PHP_URL_PATH));
489 591 $extension = strtolower($path_info['extension'] ?? '');
490 592
491 593 return in_array($extension, $valid_extensions);
492 594 }