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/ultimate-post-kit-others-plugin.php +89 -83 4.1.15 → 4.5.6 View file →
@@ -21,11 +21,11 @@
21 21 /**
22 22 * Constructor
23 23 */
24 24 public function __construct() {
25 - // Add AJAX handlers
25 + // Add AJAX handlers. This is an admin-only, plugin-install screen; the
26 + // handler must never be exposed to unauthenticated visitors.
26 27 add_action('wp_ajax_upk_get_plugins', [$this, 'ajax_get_plugins']);
27 - add_action('wp_ajax_nopriv_upk_get_plugins', [$this, 'ajax_get_plugins']);
28 28 add_action('wp_ajax_upk_install_plugin', [$this, 'install_plugin_ajax']);
29 29 }
30 30
31 31 /**
@@ -54,8 +54,9 @@
54 54 );
55 55
56 56 // Helper function for time formatting
57 57 if (!function_exists('format_last_updated_usk')) {
58 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound -- established function name relied on across the plugin family / feedback SDK; renaming would break integration.
58 59 function format_last_updated_usk($date_string) {
59 60 if (empty($date_string)) {
60 61 return __('Unknown', 'ultimate-post-kit');
61 62 }
@@ -70,58 +71,30 @@
70 71 if ($diff < 60) {
71 72 return __('Just now', 'ultimate-post-kit');
72 73 } elseif ($diff < 3600) {
73 74 $minutes = floor($diff / 60);
75 + /* translators: %d: number of minutes */
74 76 return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, 'ultimate-post-kit'), $minutes);
75 77 } elseif ($diff < 86400) {
76 78 $hours = floor($diff / 3600);
79 + /* translators: %d: number of hours */
77 80 return sprintf(_n('%d hour ago', '%d hours ago', $hours, 'ultimate-post-kit'), $hours);
78 81 } elseif ($diff < 2592000) { // 30 days
79 82 $days = floor($diff / 86400);
83 + /* translators: %d: number of days */
80 84 return sprintf(_n('%d day ago', '%d days ago', $days, 'ultimate-post-kit'), $days);
81 85 } elseif ($diff < 31536000) { // 1 year
82 86 $months = floor($diff / 2592000);
87 + /* translators: %d: number of months */
83 88 return sprintf(_n('%d month ago', '%d months ago', $months, 'ultimate-post-kit'), $months);
84 89 } else {
85 90 $years = floor($diff / 31536000);
91 + /* translators: %d: number of years */
86 92 return sprintf(_n('%d year ago', '%d years ago', $years, 'ultimate-post-kit'), $years);
87 93 }
88 94 }
89 95 }
90 96
91 - // Helper function for fallback URLs
92 - if (!function_exists('get_plugin_fallback_urls_usk')) {
93 - function get_plugin_fallback_urls_usk($plugin_slug) {
94 - // Handle different plugin slug formats
95 - if (strpos($plugin_slug, '/') !== false) {
96 - // If it's a file path like 'plugin-name/plugin-name.php', extract directory
97 - $plugin_slug_clean = dirname($plugin_slug);
98 - } else {
99 - // If it's just the plugin directory name, use it directly
100 - $plugin_slug_clean = $plugin_slug;
101 - }
102 -
103 - // Custom icon URLs for specific plugins that might not be on WordPress.org
104 - $custom_icons = [
105 - 'ar-viewer' => [
106 - 'https://ps.w.org/ar-viewer/assets/icon-256x256.gif',
107 - 'https://ps.w.org/ar-viewer/assets/icon-128x128.gif',
108 - ],
109 - ];
110 -
111 - // Return custom icons if available, otherwise use default WordPress.org URLs
112 - if (isset($custom_icons[$plugin_slug_clean])) {
113 - return $custom_icons[$plugin_slug_clean];
114 - }
115 -
116 - return [
117 - "https://ps.w.org/{$plugin_slug_clean}/assets/icon-256x256.png", // Then PNG
118 - "https://ps.w.org/{$plugin_slug_clean}/assets/icon-128x128.png", // Medium PNG
119 - "https://ps.w.org/{$plugin_slug_clean}/assets/icon-256x256.gif", // Try GIF first
120 - "https://ps.w.org/{$plugin_slug_clean}/assets/icon-128x128.gif", // Medium GIF
121 - ];
122 - }
123 - }
124 97 ?>
125 98
126 99 <div class="upk-dashboard-panel"
127 100 bdt-scrollspy="target: > div > div > .bdt-card; cls: bdt-animation-slide-bottom-small; delay: 300">
@@ -218,9 +191,9 @@
218 191 url: ajaxurl,
219 192 type: 'POST',
220 193 data: {
221 194 action: 'upk_get_plugins',
222 - nonce: '<?php echo wp_create_nonce("upk_get_plugins_nonce"); ?>'
195 + nonce: '<?php echo esc_attr( wp_create_nonce("upk_get_plugins_nonce") ); ?>'
223 196 },
224 197 success: function(response) {
225 198 if (response.success && response.data) {
226 199 if (response.data.loading) {
@@ -239,14 +212,28 @@
239 212 }
240 213 });
241 214 }
242 215
216 + // Escape remote-sourced strings before they are concatenated into
217 + // markup. The plugin catalog comes from a remote endpoint; treat it
218 + // as untrusted so a poisoned/compromised feed cannot inject HTML/JS
219 + // into the admin dashboard (the 2026 notification-feed incident).
220 + function upkEsc(s) {
221 + return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
222 + return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
223 + });
224 + }
225 + function upkSafeUrl(u) {
226 + u = String(u == null ? '' : u);
227 + return /^https?:\/\//i.test(u) ? u : '';
228 + }
229 +
243 230 // Function to render plugins
244 231 function renderPlugins(plugins) {
245 232 var html = '';
246 233
247 234 if (plugins.length === 0) {
248 - html = '<div class="bdt-text-center bdt-padding-large"><p><?php esc_html_e('No plugins available.', 'ultimate-post-kit'); ?></p></div>';
235 + html = '<div class="bdt-text-center bdt-padding-large"><p><?php echo esc_js(__('No plugins available.', 'ultimate-post-kit')); ?></p></div>';
249 236 } else {
250 237 plugins.forEach(function(plugin) {
251 238 // Skip own plugin (Ultimate Post Kit) when printing only; data still includes it for other plugins
252 239 if (plugin.slug === 'ultimate-post-kit') return;
@@ -254,39 +241,37 @@
254 241 var logoUrl = plugin.logo || '';
255 242 var pluginName = plugin.name || '';
256 243 var pluginSlug = plugin.slug || '';
257 244
258 - // Generate fallback logo URL if needed
259 - if (!logoUrl) {
260 - var actualSlug = pluginSlug.replace('.php', '').split('/')[0];
261 - logoUrl = 'https://ps.w.org/' + actualSlug + '/assets/icon-256x256.png';
262 - }
263 -
245 + // The logo URL comes from the WordPress.org API response. When it is
246 + // missing we show the local placeholder rather than guessing a remote
247 + // asset URL.
248 + var logoMarkup = upkSafeUrl(logoUrl)
249 + ? '<img src="' + upkEsc(upkSafeUrl(logoUrl)) + '" alt="' + upkEsc(pluginName) + '" class="bdt-plugin-logo" ' +
250 + 'onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">' +
251 + '<div class="default-plugin-icon" style="display:none;">📦</div>'
252 + : '<div class="default-plugin-icon" style="display:flex;">📦</div>';
253 +
264 254 html += '<div class="bdt-card bdt-card-body bdt-flex bdt-flex-middle bdt-flex-between">' +
265 255 '<div class="bdt-others-plugin-content">' +
266 256 '<div class="bdt-plugin-logo-wrap bdt-flex bdt-flex-middle">' +
267 257 '<div class="bdt-plugin-logo-container">' +
268 - '<img src="' + logoUrl + '" alt="' + pluginName + '" class="bdt-plugin-logo" ' +
269 - 'onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">' +
270 - '<div class="default-plugin-icon" style="display:none;">📦</div>' +
258 + logoMarkup +
271 259 '</div>' +
272 260 '<div class="bdt-others-plugin-user-wrap bdt-flex bdt-flex-middle">' +
273 - '<h1 class="upk-feature-title">' + pluginName + '</h1>' +
261 + '<h1 class="upk-feature-title">' + upkEsc(pluginName) + '</h1>' +
274 262 '</div>' +
275 263 '</div>' +
276 264 '<div class="bdt-others-plugin-content-text bdt-margin-top">';
277 -
278 - if (plugin.description) {
279 - html += '<p>' + plugin.description + '</p>';
280 - }
281 -
265 +
282 266 // Active installs
267 + var installsCount = Number(plugin.active_installs_count) || 0;
283 268 html += '<span class="active-installs bdt-margin-small-top">' +
284 - '<?php esc_html_e("Active Installs: ", "ultimate-post-kit"); ?> ';
285 - if (plugin.active_installs_count > 0) {
286 - html += '<span class="installs-count">' + plugin.active_installs_count.toLocaleString() + '+</span>';
269 + '<?php echo esc_js(__('Active Installs: ', 'ultimate-post-kit')); ?> ';
270 + if (installsCount > 0) {
271 + html += '<span class="installs-count">' + upkEsc(installsCount.toLocaleString()) + '+</span>';
287 272 } else {
288 - html += '<span class="installs-count">Fewer than 10</span>';
273 + html += '<span class="installs-count"><?php echo esc_js(__('Fewer than 10', 'ultimate-post-kit')); ?></span>';
289 274 }
290 275 html += '</span>';
291 276
292 277 // Rating
@@ -292,29 +277,32 @@
292 277 // Rating
293 278 html += '<div class="bdt-others-plugin-rating bdt-margin-small-top bdt-flex bdt-flex-middle">' +
294 279 '<span class="bdt-others-plugin-rating-stars">';
295 280
296 - var rating = parseFloat(plugin.rating) || 0;
281 + // Clamp to 0-5 so malformed data cannot emit a runaway number of stars.
282 + var rating = Math.min(5, Math.max(0, parseFloat(plugin.rating) || 0));
297 283 var fullStars = Math.floor(rating);
298 284 var hasHalfStar = (rating - fullStars) >= 0.5;
299 285 var emptyStars = 5 - fullStars - (hasHalfStar ? 1 : 0);
300 -
301 - for (var i = 0; i < fullStars; i++) {
286 + var i;
287 +
288 + for (i = 0; i < fullStars; i++) {
302 289 html += '<i class="dashicons dashicons-star-filled"></i>';
303 290 }
304 291 if (hasHalfStar) {
305 292 html += '<i class="dashicons dashicons-star-half"></i>';
306 293 }
307 - for (var i = 0; i < emptyStars; i++) {
294 + for (i = 0; i < emptyStars; i++) {
308 295 html += '<i class="dashicons dashicons-star-empty"></i>';
309 296 }
310 297
311 298 html += '</span>' +
312 299 '<span class="bdt-others-plugin-rating-text bdt-margin-small-left">' +
313 - rating + ' <?php esc_html_e("out of 5 stars.", "ultimate-post-kit"); ?>';
300 + rating + ' <?php echo esc_js(__('out of 5 stars.', 'ultimate-post-kit')); ?>';
314 301
315 - if (plugin.num_ratings > 0) {
316 - html += '<span class="rating-count">(' + plugin.num_ratings.toLocaleString() + ' <?php esc_html_e("ratings", "ultimate-post-kit"); ?>)</span>';
302 + var numRatings = Number(plugin.num_ratings) || 0;
303 + if (numRatings > 0) {
304 + html += '<span class="rating-count">(' + upkEsc(numRatings.toLocaleString()) + ' <?php echo esc_js(__('ratings', 'ultimate-post-kit')); ?>)</span>';
317 305 }
318 306
319 307 html += '</span></div>';
320 308
@@ -320,9 +308,9 @@
320 308
321 309 // Downloads
322 310 if (plugin.downloaded_formatted) {
323 311 html += '<div class="bdt-others-plugin-downloads bdt-margin-small-top">' +
324 - '<span><?php esc_html_e("Downloads: ", "ultimate-post-kit"); ?>' + plugin.downloaded_formatted + '</span>' +
312 + '<span><?php echo esc_js(__('Downloads: ', 'ultimate-post-kit')); ?>' + upkEsc(plugin.downloaded_formatted) + '</span>' +
325 313 '</div>';
326 314 }
327 315
328 316 // Last updated
@@ -327,9 +315,9 @@
327 315
328 316 // Last updated
329 317 if (plugin.last_updated_formatted) {
330 318 html += '<div class="bdt-others-plugin-updated bdt-margin-small-top">' +
331 - '<span><?php esc_html_e("Last Updated: ", "ultimate-post-kit"); ?>' + plugin.last_updated_formatted + '</span>' +
319 + '<span><?php echo esc_js(__('Last Updated: ', 'ultimate-post-kit')); ?>' + upkEsc(plugin.last_updated_formatted) + '</span>' +
332 320 '</div>';
333 321 }
334 322
335 323 html += '</div></div>' +
@@ -338,24 +326,28 @@
338 326 // Show different buttons based on plugin status
339 327 if (plugin.status === 'active') {
340 328 html += '<span class="bdt-button bdt-button-success bdt-disabled">' +
341 329 '<span class="dashicons dashicons-yes"></span> ' +
342 - '<?php esc_html_e("Active", "ultimate-post-kit"); ?>' +
330 + '<?php echo esc_js(__('Active', 'ultimate-post-kit')); ?>' +
343 331 '</span>';
344 332 } else if (plugin.status === 'installed') {
345 - var activateUrl = '<?php echo admin_url("plugins.php?action=activate&plugin="); ?>' + plugin.plugin_file + '&_wpnonce=' + plugin.activate_nonce;
346 - html += '<a class="bdt-button bdt-welcome-button" href="' + activateUrl + '">' +
347 - '<?php esc_html_e("Activate", "ultimate-post-kit"); ?>' +
333 + // URL-encode the query values: plugin_file contains slashes and
334 + // both parts land inside an href attribute.
335 + var activateUrl = <?php echo wp_json_encode( esc_url_raw( admin_url( 'plugins.php?action=activate&plugin=' ) ) ); ?> +
336 + encodeURIComponent(plugin.plugin_file || '') +
337 + '&_wpnonce=' + encodeURIComponent(plugin.activate_nonce || '');
338 + html += '<a class="bdt-button bdt-welcome-button" href="' + upkEsc(activateUrl) + '">' +
339 + '<?php echo esc_js(__('Activate', 'ultimate-post-kit')); ?>' +
348 340 '</a>';
349 341 } else {
350 - html += '<button class="bdt-button bdt-welcome-button upk-install-plugin" data-plugin-slug="' + pluginSlug + '" data-nonce="<?php echo wp_create_nonce('upk_install_plugin_nonce'); ?>">' +
351 - '<?php esc_html_e("Install", "ultimate-post-kit"); ?>' +
342 + html += '<button type="button" class="bdt-button bdt-welcome-button upk-install-plugin" data-plugin-slug="' + upkEsc(pluginSlug) + '" data-nonce="<?php echo esc_attr( wp_create_nonce('upk_install_plugin_nonce') ); ?>">' +
343 + '<?php echo esc_js(__('Install', 'ultimate-post-kit')); ?>' +
352 344 '</button>';
353 345 }
354 346
355 - if (plugin.homepage) {
356 - html += '<a class="bdt-button bdt-dashboard-sec-btn" target="_blank" href="' + plugin.homepage + '">' +
357 - '<?php esc_html_e("Learn More", "ultimate-post-kit"); ?>' +
347 + if (plugin.homepage && upkSafeUrl(plugin.homepage)) {
348 + html += '<a class="bdt-button bdt-dashboard-sec-btn" target="_blank" rel="noopener noreferrer" href="' + upkEsc(upkSafeUrl(plugin.homepage)) + '">' +
349 + '<?php echo esc_js(__('Learn More', 'ultimate-post-kit')); ?>' +
358 350 '</a>';
359 351 }
360 352
361 353 html += '</div></div>';
@@ -363,10 +355,13 @@
363 355 }
364 356
365 357 $list.html(html);
366 358
367 - // Handle plugin action buttons
368 - $('.upk-install-plugin').on('click', function(e) {
359 + // Handle plugin action buttons. Delegated from the list and
360 + // namespaced+unbound first: renderPlugins() runs again on every
361 + // retry, and a plain global bind stacked one handler per render,
362 + // firing duplicate install requests for a single click.
363 + $list.off('click.upkInstall').on('click.upkInstall', '.upk-install-plugin', function(e) {
369 364 e.preventDefault();
370 365
371 366 var $button = $(this);
372 367 var pluginSlug = $button.data('plugin-slug');
@@ -379,9 +374,9 @@
379 374 .addClass('bdt-installing');
380 375
381 376 // Perform AJAX request
382 377 $.ajax({
383 - url: '<?php echo admin_url('admin-ajax.php'); ?>',
378 + url: '<?php echo esc_url( admin_url('admin-ajax.php') ); ?>',
384 379 type: 'POST',
385 380 data: {
386 381 action: 'upk_install_plugin',
387 382 plugin_slug: pluginSlug,
@@ -454,12 +449,15 @@
454 449 '<div class="upk-loading-dot"></div>' +
455 450 '<div class="upk-loading-dot"></div>' +
456 451 '</div>' +
457 452 '</div>' +
458 - '<p class="bdt-margin-small-top bdt-text-muted"><?php esc_html_e("Loading plugin data...", "ultimate-post-kit"); ?></p>' +
453 + '<p class="bdt-margin-small-top bdt-text-muted"><?php echo esc_js(__('Loading plugin data...', 'ultimate-post-kit')); ?></p>' +
459 454 '</div>'
460 455 );
461 - $list.show();
456 + // Set the display explicitly: the list is a CSS grid, and jQuery's
457 + // .show() can resolve the inline display:none to "block", which would
458 + // flatten the card grid into a single stacked column.
459 + $list.css('display', 'grid');
462 460 }
463 461
464 462 // Function to show error
465 463 function showError() {
@@ -482,13 +480,20 @@
482 480 /**
483 481 * AJAX handler for getting plugins data
484 482 */
485 483 public function ajax_get_plugins() {
486 - // Verify nonce
484 + // Verify nonce. Respond with JSON -- the caller parses the response as
485 + // JSON, so wp_die() here would surface as a generic "unable to load".
487 486 if (!check_ajax_referer('upk_get_plugins_nonce', 'nonce', false)) {
488 - wp_die(__('Security check failed.', 'ultimate-post-kit'));
487 + wp_send_json_error(['message' => __('Security check failed.', 'ultimate-post-kit')], 403);
489 488 }
490 489
490 + // This data is only ever used on the plugin-install screen; gate it to
491 + // users who could act on it rather than exposing it to any visitor.
492 + if (!current_user_can('install_plugins')) {
493 + wp_send_json_error(['message' => __('You do not have permission to do this.', 'ultimate-post-kit')], 403);
494 + }
495 +
491 496 // Get cached data
492 497 $plugins_data = \UltimatePostKit\SetupWizard\Remote_Data_Handler::get_remote_plugins();
493 498
494 499 // If cache is empty, try to fetch immediately (but don't block)
@@ -516,9 +521,10 @@
516 521 * AJAX handler for plugin installation
517 522 */
518 523 public function install_plugin_ajax() {
519 524 // Check nonce
520 - if (!wp_verify_nonce($_POST['nonce'], 'upk_install_plugin_nonce')) {
525 + $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
526 + if (!wp_verify_nonce( $nonce, 'upk_install_plugin_nonce')) {
521 527 wp_send_json_error(['message' => __('Security check failed', 'ultimate-post-kit')]);
522 528 }
523 529
524 530 // Check user capability
@@ -525,9 +531,9 @@
525 531 if (!current_user_can('install_plugins')) {
526 532 wp_send_json_error(['message' => __('You do not have permission to install plugins', 'ultimate-post-kit')]);
527 533 }
528 534
529 - $plugin_slug = sanitize_text_field($_POST['plugin_slug']);
535 + $plugin_slug = isset($_POST['plugin_slug']) ? sanitize_text_field(wp_unslash($_POST['plugin_slug'])) : '';
530 536
531 537 if (empty($plugin_slug)) {
532 538 wp_send_json_error(['message' => __('Plugin slug is required', 'ultimate-post-kit')]);
533 539 }