| @@ -8,8 +8,10 @@ | ||
| 8 | 8 | if (!defined('ABSPATH')) { |
| 9 | 9 | exit; |
| 10 | 10 | } |
| 11 | 11 | |
| 12 | +// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- template partial included within a method; variables are method-scoped, not global. | |
| 13 | + | |
| 12 | 14 | // Include the required classes |
| 13 | 15 | require_once __DIR__ . '/../class-plugin-integration-helper.php'; |
| 14 | 16 | require_once __DIR__ . '/../class-remote-data-handler.php'; |
| 15 | 17 | |
| @@ -30,20 +32,25 @@ | ||
| 30 | 32 | if ($diff < 60) { |
| 31 | 33 | return __('Just now', 'ultimate-post-kit'); |
| 32 | 34 | } elseif ($diff < 3600) { |
| 33 | 35 | $minutes = floor($diff / 60); |
| 36 | + /* translators: %d: number of minutes */ | |
| 34 | 37 | return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, 'ultimate-post-kit'), $minutes); |
| 35 | 38 | } elseif ($diff < 86400) { |
| 36 | 39 | $hours = floor($diff / 3600); |
| 40 | + /* translators: %d: number of hours */ | |
| 37 | 41 | return sprintf(_n('%d hour ago', '%d hours ago', $hours, 'ultimate-post-kit'), $hours); |
| 38 | 42 | } elseif ($diff < 2592000) { // 30 days |
| 39 | 43 | $days = floor($diff / 86400); |
| 44 | + /* translators: %d: number of days */ | |
| 40 | 45 | return sprintf(_n('%d day ago', '%d days ago', $days, 'ultimate-post-kit'), $days); |
| 41 | 46 | } elseif ($diff < 31536000) { // 1 year |
| 42 | 47 | $months = floor($diff / 2592000); |
| 48 | + /* translators: %d: number of months */ | |
| 43 | 49 | return sprintf(_n('%d month ago', '%d months ago', $months, 'ultimate-post-kit'), $months); |
| 44 | 50 | } else { |
| 45 | 51 | $years = floor($diff / 31536000); |
| 52 | + /* translators: %d: number of years */ | |
| 46 | 53 | return sprintf(_n('%d year ago', '%d years ago', $years, 'ultimate-post-kit'), $years); |
| 47 | 54 | } |
| 48 | 55 | } |
| 49 | 56 | } |
| @@ -48,38 +55,8 @@ | ||
| 48 | 55 | } |
| 49 | 56 | } |
| 50 | 57 | |
| 51 | 58 | // Helper function for fallback URLs |
| 52 | -if (!function_exists('get_plugin_fallback_urls_usk')) { | |
| 53 | - function get_plugin_fallback_urls_usk($plugin_slug) { | |
| 54 | - // Handle different plugin slug formats | |
| 55 | - if (strpos($plugin_slug, '/') !== false) { | |
| 56 | - // If it's a file path like 'plugin-name/plugin-name.php', extract directory | |
| 57 | - $plugin_slug_clean = dirname($plugin_slug); | |
| 58 | - } else { | |
| 59 | - // If it's just the plugin directory name, use it directly | |
| 60 | - $plugin_slug_clean = $plugin_slug; | |
| 61 | - } | |
| 62 | - | |
| 63 | - // Custom icon URLs for specific plugins that might not be on WordPress.org | |
| 64 | - $custom_icons = [ | |
| 65 | - 'ar-viewer' => [ | |
| 66 | - 'https://ps.w.org/ar-viewer/assets/icon-256x256.gif', | |
| 67 | - 'https://ps.w.org/ar-viewer/assets/icon-128x128.gif', | |
| 68 | - ], | |
| 69 | - ]; | |
| 70 | - | |
| 71 | - // Return custom icons if available, otherwise use default WordPress.org URLs | |
| 72 | - if (isset($custom_icons[$plugin_slug_clean])) { | |
| 73 | - return $custom_icons[$plugin_slug_clean]; | |
| 74 | - } | |
| 75 | - | |
| 76 | - return [ | |
| 77 | - "https://ps.w.org/{$plugin_slug_clean}/assets/icon-256x256.png", // Large PNG | |
| 78 | - "https://ps.w.org/{$plugin_slug_clean}/assets/icon-128x128.png", // Medium PNG | |
| 79 | - ]; | |
| 80 | - } | |
| 81 | -} | |
| 82 | 59 | |
| 83 | 60 | // Define plugin slugs |
| 84 | 61 | $plugin_slugs = array( |
| 85 | 62 | 'bdthemes-element-pack-lite', |
| @@ -177,13 +154,10 @@ | ||
| 177 | 154 | echo '<img src="' . esc_url($logo_url) . '" alt="' . esc_attr($plugin_name) . '" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">'; |
| 178 | 155 | echo '<div class="default-plugin-icon" style="display:none;">📦</div>'; |
| 179 | 156 | } else { |
| 180 | 157 | // Generate fallback URLs for WordPress.org |
| 181 | - $actual_slug = (strpos($plugin_slug, '/') !== false) ? dirname($plugin_slug) : $plugin_slug; | |
| 182 | - $fallback_urls = get_plugin_fallback_urls_usk($actual_slug); | |
| 183 | - | |
| 184 | - echo '<img src="' . esc_url($fallback_urls[0]) . '" alt="' . esc_attr($plugin_name) . '" onerror="this.style.display=\'none\'; this.nextElementSibling.style.display=\'flex\';">'; | |
| 185 | - echo '<div class="default-plugin-icon" style="display:none;">📦</div>'; | |
| 158 | + // No icon in the API response, show the local placeholder. | |
| 159 | + echo '<div class="default-plugin-icon" style="display:flex;">📦</div>'; | |
| 186 | 160 | } |
| 187 | 161 | ?> |
| 188 | 162 | </span> |
| 189 | 163 | |
| @@ -199,9 +173,9 @@ | ||
| 199 | 173 | <?php |
| 200 | 174 | if (!$is_active) : ?> |
| 201 | 175 | <label class="switch"> |
| 202 | 176 | <input type="checkbox" class="plugin-slider-checkbox" <?php echo $plugin_recommended ? 'checked' : ''; ?> |
| 203 | - name="plugins[]<?php echo isset($plugin['slug']) ? wp_kses_post($plugin['slug']) : ''; ?>"> | |
| 177 | + name="plugins[]<?php echo isset($plugin['slug']) ? esc_attr($plugin['slug']) : ''; ?>"> | |
| 204 | 178 | <span class="slider round"></span> |
| 205 | 179 | </label> |
| 206 | 180 | <?php |
| 207 | 181 | endif; |
| @@ -209,9 +183,9 @@ | ||
| 209 | 183 | </div> |
| 210 | 184 | </span> |
| 211 | 185 | <div class="bdt-flex bdt-flex-middle"> |
| 212 | 186 | <span class="bdt-plugin-name"> |
| 213 | - <?php echo wp_kses_post($plugin['name']); ?> | |
| 187 | + <?php echo esc_html($plugin['name']); ?> | |
| 214 | 188 | </span> |
| 215 | 189 | </div> |
| 216 | 190 | |
| 217 | 191 | <span class="active-installs"> |
| @@ -224,9 +198,9 @@ | ||
| 224 | 198 | ?> |
| 225 | 199 | </span> |
| 226 | 200 | |
| 227 | 201 | <?php if (isset($plugin['downloaded_formatted']) && !empty($plugin['downloaded_formatted'])): ?> |
| 228 | - <span class="downloads"><?php esc_html_e('Downloads: ', 'ultimate-post-kit'); echo wp_kses_post($plugin['downloaded_formatted']); ?></span> | |
| 202 | + <span class="downloads"><?php esc_html_e('Downloads: ', 'ultimate-post-kit'); echo esc_html($plugin['downloaded_formatted']); ?></span> | |
| 229 | 203 | <?php endif; ?> |
| 230 | 204 | |
| 231 | 205 | <div class="rating-section"> |
| 232 | 206 | <div class="wporg-ratings" title="<?php echo esc_attr($plugin['rating'] ?? '0'); ?> out of 5 stars" style="color:var(--wp--preset--color--pomegrade-1, #e26f56);"> |
| @@ -349,9 +323,9 @@ | ||
| 349 | 323 | url: ajaxurl, |
| 350 | 324 | type: 'POST', |
| 351 | 325 | data: { |
| 352 | 326 | action: 'upk_get_plugins', |
| 353 | - nonce: '<?php echo wp_create_nonce('upk_get_plugins_nonce'); ?>' | |
| 327 | + nonce: '<?php echo esc_attr( wp_create_nonce('upk_get_plugins_nonce') ); ?>' | |
| 354 | 328 | }, |
| 355 | 329 | success: function(response) { |
| 356 | 330 | if (response.success && response.data.plugins) { |
| 357 | 331 | // Hide the initial loading div |
| @@ -367,8 +341,33 @@ | ||
| 367 | 341 | } |
| 368 | 342 | }); |
| 369 | 343 | } |
| 370 | 344 | |
| 345 | + // Translatable strings used by the dynamically rendered plugin list. | |
| 346 | + const upkI18n = <?php echo wp_json_encode( array( | |
| 347 | + 'noPlugins' => __( 'No plugins found.', 'ultimate-post-kit' ), | |
| 348 | + 'recommended' => __( 'Recommended', 'ultimate-post-kit' ), | |
| 349 | + 'active' => __( 'ACTIVE', 'ultimate-post-kit' ), | |
| 350 | + /* translators: %s: number of active installs, or the "Fewer than 10" phrase. */ | |
| 351 | + 'activeInstalls' => __( 'Active Installs: %s', 'ultimate-post-kit' ), | |
| 352 | + 'fewerThanTen' => __( 'Fewer than 10', 'ultimate-post-kit' ), | |
| 353 | + /* translators: %s: formatted download count. */ | |
| 354 | + 'downloads' => __( 'Downloads: %s', 'ultimate-post-kit' ), | |
| 355 | + /* translators: %s: plugin rating, e.g. 4.5. */ | |
| 356 | + 'ratingTitle' => __( '%s out of 5 stars', 'ultimate-post-kit' ), | |
| 357 | + /* translators: %s: plugin rating, e.g. 4.5. */ | |
| 358 | + 'ratingText' => __( '%s out of 5 stars.', 'ultimate-post-kit' ), | |
| 359 | + /* translators: %s: number of ratings. */ | |
| 360 | + 'ratingCount' => __( '(%s ratings)', 'ultimate-post-kit' ), | |
| 361 | + /* translators: %s: how long ago the plugin was updated. */ | |
| 362 | + 'lastUpdated' => __( 'Last Updated: %s', 'ultimate-post-kit' ), | |
| 363 | + ), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT ); ?>; | |
| 364 | + | |
| 365 | + // Minimal printf-style substitution so translators keep control of word order. | |
| 366 | + function upkFormat(template, value) { | |
| 367 | + return String(template).replace('%s', value); | |
| 368 | + } | |
| 369 | + | |
| 371 | 370 | // Function to render plugin list |
| 372 | 371 | function renderPluginList(plugins) { |
| 373 | 372 | const $pluginList = $('#upk-integration-plugin-list'); |
| 374 | 373 | let html = ''; |
| @@ -373,9 +372,9 @@ | ||
| 373 | 372 | const $pluginList = $('#upk-integration-plugin-list'); |
| 374 | 373 | let html = ''; |
| 375 | 374 | |
| 376 | 375 | if (plugins.length === 0) { |
| 377 | - html = '<div class="upk-no-plugins" style="text-align: center; padding: 40px;"><p>No plugins found.</p></div>'; | |
| 376 | + html = `<div class="upk-no-plugins" style="text-align: center; padding: 40px;"><p>${upkEsc(upkI18n.noPlugins)}</p></div>`; | |
| 378 | 377 | } else { |
| 379 | 378 | plugins.forEach(function(plugin) { |
| 380 | 379 | // Skip own plugin (Ultimate Post Kit) when printing only; data still includes it for other plugins |
| 381 | 380 | if (plugin.slug === 'ultimate-post-kit') return; |
| @@ -382,19 +381,19 @@ | ||
| 382 | 381 | const isActive = plugin.status === 'active'; |
| 383 | 382 | const isRecommended = plugin.recommended && !isActive; |
| 384 | 383 | |
| 385 | 384 | html += ` |
| 386 | - <label class="plugin-item" data-slug="${plugin.slug}"> | |
| 385 | + <label class="plugin-item" data-slug="${upkEsc(plugin.slug)}"> | |
| 387 | 386 | <span class="bdt-flex bdt-flex-middle bdt-flex-between bdt-margin-small-bottom"> |
| 388 | 387 | <span class="bdt-plugin-logo"> |
| 389 | 388 | ${generatePluginLogo(plugin)} |
| 390 | 389 | </span> |
| 391 | 390 | <div class="bdt-plugin-badge-switch-wrap"> |
| 392 | - ${isRecommended ? '<span class="recommended-badge">Recommended</span>' : ''} | |
| 393 | - ${isActive ? '<span class="active-badge">ACTIVE</span>' : ''} | |
| 391 | + ${isRecommended ? `<span class="recommended-badge">${upkEsc(upkI18n.recommended)}</span>` : ''} | |
| 392 | + ${isActive ? `<span class="active-badge">${upkEsc(upkI18n.active)}</span>` : ''} | |
| 394 | 393 | ${!isActive ? ` |
| 395 | 394 | <label class="switch"> |
| 396 | - <input type="checkbox" class="plugin-slider-checkbox" ${plugin.recommended ? 'checked' : ''} name="plugins[]${plugin.slug}"> | |
| 395 | + <input type="checkbox" class="plugin-slider-checkbox" ${plugin.recommended ? 'checked' : ''} name="plugins[]${upkEsc(plugin.slug)}"> | |
| 397 | 396 | <span class="slider round"></span> |
| 398 | 397 | </label> |
| 399 | 398 | ` : ''} |
| 400 | 399 | </div> |
| @@ -399,25 +398,24 @@ | ||
| 399 | 398 | ` : ''} |
| 400 | 399 | </div> |
| 401 | 400 | </span> |
| 402 | 401 | <div class="bdt-flex bdt-flex-middle"> |
| 403 | - <span class="bdt-plugin-name">${plugin.name}</span> | |
| 402 | + <span class="bdt-plugin-name">${upkEsc(plugin.name)}</span> | |
| 404 | 403 | </div> |
| 405 | 404 | <span class="active-installs"> |
| 406 | - Active Installs: | |
| 407 | - <span class="installs-count">${plugin.active_installs_count > 0 ? plugin.active_installs_count.toLocaleString() + '+' : 'Fewer than 10'}</span> | |
| 405 | + ${upkFormat(upkEsc(upkI18n.activeInstalls), `<span class="installs-count">${plugin.active_installs_count > 0 ? upkEsc(plugin.active_installs_count.toLocaleString() + '+') : upkEsc(upkI18n.fewerThanTen)}</span>`)} | |
| 408 | 406 | </span> |
| 409 | - ${plugin.downloaded_formatted ? `<span class="downloads">Downloads: ${plugin.downloaded_formatted}</span>` : ''} | |
| 407 | + ${plugin.downloaded_formatted ? `<span class="downloads">${upkFormat(upkEsc(upkI18n.downloads), upkEsc(plugin.downloaded_formatted))}</span>` : ''} | |
| 410 | 408 | <div class="rating-section"> |
| 411 | - <div class="wporg-ratings" title="${plugin.rating} out of 5 stars" style="color:var(--wp--preset--color--pomegrade-1, #e26f56);"> | |
| 409 | + <div class="wporg-ratings" title="${upkEsc(upkFormat(upkI18n.ratingTitle, plugin.rating))}" style="color:var(--wp--preset--color--pomegrade-1, #e26f56);"> | |
| 412 | 410 | ${generateStarRating(plugin.rating)} |
| 413 | 411 | </div> |
| 414 | 412 | <span class="rating-text"> |
| 415 | - ${plugin.rating} out of 5 stars. | |
| 416 | - ${plugin.num_ratings > 0 ? `<span class="rating-count">(${plugin.num_ratings.toLocaleString()} ratings)</span>` : ''} | |
| 413 | + ${upkEsc(upkFormat(upkI18n.ratingText, plugin.rating))} | |
| 414 | + ${plugin.num_ratings > 0 ? `<span class="rating-count">${upkEsc(upkFormat(upkI18n.ratingCount, plugin.num_ratings.toLocaleString()))}</span>` : ''} | |
| 417 | 415 | </span> |
| 418 | 416 | </div> |
| 419 | - ${plugin.last_updated_formatted ? `<span class="last-updated">Last Updated: ${plugin.last_updated_formatted}</span>` : ''} | |
| 417 | + ${plugin.last_updated_formatted ? `<span class="last-updated">${upkFormat(upkEsc(upkI18n.lastUpdated), upkEsc(plugin.last_updated_formatted))}</span>` : ''} | |
| 420 | 418 | </label> |
| 421 | 419 | `; |
| 422 | 420 | }); |
| 423 | 421 | } |
| @@ -424,17 +422,33 @@ | ||
| 424 | 422 | |
| 425 | 423 | $pluginList.html(html); |
| 426 | 424 | } |
| 427 | 425 | |
| 426 | + // Escape remote-sourced strings before they are concatenated into markup. The plugin | |
| 427 | + // catalog comes from a remote endpoint; treat it as untrusted so a poisoned or | |
| 428 | + // compromised feed cannot inject HTML/JS into the admin dashboard. Note that | |
| 429 | + // Remote_Data_Handler::decode_api_text() html_entity_decode()s these fields, so they | |
| 430 | + // arrive here already un-escaped. | |
| 431 | + function upkEsc(s) { | |
| 432 | + return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { | |
| 433 | + return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]; | |
| 434 | + }); | |
| 435 | + } | |
| 436 | + | |
| 437 | + function upkSafeUrl(u) { | |
| 438 | + u = String(u == null ? '' : u); | |
| 439 | + return /^https?:\/\//i.test(u) ? u : ''; | |
| 440 | + } | |
| 441 | + | |
| 428 | 442 | // Helper function to generate plugin logo |
| 429 | 443 | function generatePluginLogo(plugin) { |
| 430 | 444 | if (plugin.logo && plugin.logo.match(/^https?:\/\//)) { |
| 431 | - return `<img src="${plugin.logo}" alt="${plugin.name}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"> | |
| 445 | + return `<img src="${upkEsc(upkSafeUrl(plugin.logo))}" alt="${upkEsc(plugin.name)}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"> | |
| 432 | 446 | <div class="default-plugin-icon" style="display:none;">📦</div>`; |
| 433 | 447 | } else { |
| 434 | - const slug = plugin.slug.includes('/') ? plugin.slug.split('/')[0] : plugin.slug; | |
| 435 | - return `<img src="https://ps.w.org/${slug}/assets/icon-256x256.png" alt="${plugin.name}" onerror="this.style.display='none'; this.nextElementSibling.style.display='flex';"> | |
| 436 | - <div class="default-plugin-icon" style="display:none;">📦</div>`; | |
| 448 | + // No icon supplied by the data source — show the local placeholder | |
| 449 | + // rather than offloading an image request to a remote host. | |
| 450 | + return `<div class="default-plugin-icon" style="display:flex;">📦</div>`; | |
| 437 | 451 | } |
| 438 | 452 | } |
| 439 | 453 | |
| 440 | 454 | // Helper function to generate star rating |
| @@ -466,9 +480,9 @@ | ||
| 466 | 480 | |
| 467 | 481 | // Show error in plugin list |
| 468 | 482 | $pluginList.html(` |
| 469 | 483 | <div class="upk-error-state" style="text-align: center; padding: 40px;"> |
| 470 | - <p style="color: #d63638;">${message}</p> | |
| 484 | + <p style="color: #d63638;">${upkEsc(message)}</p> | |
| 471 | 485 | <button type="button" class="bdt-button bdt-button-secondary" onclick="location.reload()">Retry</button> |
| 472 | 486 | </div> |
| 473 | 487 | `); |
| 474 | 488 | } |