PluginProbe
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets / 4.5.3
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets v4.5.3
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 4.1.3 4.1.4 All 146 releases
ultimate-post-kit / includes / setup-wizard / class-remote-data-handler.php

class-remote-data-handler.php in Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets 4.5.3, at includes/setup-wizard/class-remote-data-handler.php

623 lines 21.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ultimate Post Kit Remote Data Handler
4 *
5 * Handles remote API data loading with proper caching and background processing
6 * to prevent blocking admin pages.
7 */
8
9 namespace UltimatePostKit\SetupWizard;
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15
16
17 class Remote_Data_Handler {
18
19 /**
20 * Cache duration in seconds (7 days)
21 */
22 const CACHE_DURATION = 7 * DAY_IN_SECONDS;
23
24 /**
25 * Transient key for remote plugins data
26 */
27 const CACHE_KEY = 'bdt_remote_plugins_data';
28
29 /**
30 * Cron hook name for background fetch
31 */
32 const CRON_HOOK = 'bdt_fetch_remote_plugins_cron';
33
34 /**
35 * Initialize the remote data handler
36 */
37 public static function init() {
38 add_action('init', [__CLASS__, 'schedule_cron']);
39 add_action(self::CRON_HOOK, [__CLASS__, 'cron_fetch_plugins']);
40 // Admin-only plugin-install data; never expose to unauthenticated visitors.
41 add_action('wp_ajax_upk_get_plugins', [__CLASS__, 'ajax_get_plugins']);
42 }
43
44 /**
45 * WP-Cron callback for fetching plugins
46 */
47 public static function cron_fetch_plugins() {
48 self::fetch_remote_plugins_now();
49 }
50
51 /**
52 * Check if we're on the Ultimate Post Kit options page
53 *
54 * @return bool True if on Ultimate Post Kit options page
55 */
56 public static function is_element_pack_page() {
57 if (!is_admin()) {
58 return false;
59 }
60
61 // Check if this is an AJAX request for our plugins
62 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check of the AJAX action name, no form data processed.
63 if (wp_doing_ajax() && isset($_REQUEST['action'])) {
64 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only routing check of the AJAX action name, no form data processed.
65 $action = sanitize_text_field(wp_unslash($_REQUEST['action']));
66 if (in_array($action, ['upk_get_plugins'])) {
67 return true;
68 }
69 }
70
71 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only check of the current admin page slug, no form data processed.
72 $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
73 return $page === 'ultimate_post_kit_options';
74 }
75
76 /**
77 * Get remote plugins data from cache
78 *
79 * @return array Cached plugins data or empty array if not available
80 */
81 public static function get_remote_plugins() {
82 $cached_data = get_transient(self::CACHE_KEY);
83
84 if ($cached_data !== false) {
85 return $cached_data;
86 }
87
88 // If no cache exists, schedule background fetch and return empty array
89 self::schedule_remote_fetch();
90
91 return [];
92 }
93
94 /**
95 * Schedule a background fetch via WP-Cron
96 *
97 * @return bool True if successfully scheduled
98 */
99 public static function schedule_remote_fetch() {
100 // Schedule to run immediately if not already scheduled
101 if (!wp_next_scheduled(self::CRON_HOOK)) {
102 wp_schedule_single_event(time(), self::CRON_HOOK);
103 return true;
104 }
105
106 return false;
107 }
108
109 /**
110 * Fetch remote plugins data immediately (for background processing only)
111 *
112 * @return array|false Plugins data or false on failure
113 */
114 public static function fetch_remote_plugins_now() {
115 // Define plugin slugs to fetch
116 $plugin_slugs = [
117 'bdthemes-element-pack-lite',
118 'bdthemes-prime-slider-lite',
119 'ultimate-post-kit',
120 'ultimate-store-kit',
121 'zoloblocks',
122 'pixel-gallery',
123 'live-copy-paste',
124 'spin-wheel',
125 'ai-image',
126 'dark-reader',
127 'ar-viewer',
128 'smart-admin-assistant',
129 'website-accessibility',
130 ];
131
132 $results = [];
133 $errors = [];
134
135 foreach ($plugin_slugs as $slug) {
136 // Direct API fetch - no external dependencies
137 $data = self::fetch_plugin_from_api($slug);
138 if ($data !== false) {
139 $results[$slug] = $data;
140 } else {
141 $errors[] = $slug;
142 }
143 }
144
145 // Cache the results for 7 days
146 set_transient(self::CACHE_KEY, $results, self::CACHE_DURATION);
147
148 return $results;
149 }
150
151 /**
152 * AJAX handler for getting plugins data
153 */
154 public static function ajax_get_plugins() {
155 // Verify nonce for security
156 if (!check_ajax_referer('upk_get_plugins_nonce', 'nonce', false)) {
157 wp_send_json_error(['message' => __('Security check failed.', 'ultimate-post-kit')], 403);
158 }
159
160 // Gate to users who could act on it; also prevents the synchronous
161 // remote-fetch trigger below from being reachable without capability.
162 if (!current_user_can('install_plugins')) {
163 wp_send_json_error(['message' => __('You do not have permission to do this.', 'ultimate-post-kit')], 403);
164 }
165
166 // Get cached data
167 $plugins_data = self::get_remote_plugins();
168
169 // If cache is empty, fetch immediately for better UX
170 if (empty($plugins_data)) {
171 // Try to fetch data immediately (this is an AJAX request, so it's async already)
172 $plugins_data = self::fetch_remote_plugins_now();
173
174 // If still empty after fetch, schedule background cron for retry
175 if (empty($plugins_data)) {
176 self::schedule_remote_fetch();
177
178 // Return empty response with flag indicating data is loading
179 wp_send_json_success([
180 'plugins' => [],
181 'loading' => true,
182 'message' => __('Loading plugin data...', 'ultimate-post-kit')
183 ]);
184 }
185 }
186
187 // Get recommended flags from Plugin_Integration_Helper (key may be 'slug' or 'slug/script.php')
188 $recommended_by_slug = [];
189 $helper_file = __DIR__ . '/class-plugin-integration-helper.php';
190 if (file_exists($helper_file)) {
191 require_once $helper_file;
192 $predefined = \UltimatePostKit\SetupWizard\Plugin_Integration_Helper::get_predefined_plugins();
193 foreach ($predefined as $key => $config) {
194 $dir = (strpos($key, '/') !== false) ? dirname($key) : $key;
195 $recommended_by_slug[$dir] = !empty($config['recommended']);
196 }
197 }
198
199 // Format the response for frontend use
200 $formatted_plugins = [];
201 foreach ($plugins_data as $slug => $data) {
202 // Check plugin status
203 $plugin_status = self::get_plugin_status_by_slug($slug);
204 $plugin_file = self::get_plugin_file_by_slug($slug);
205
206 // Format the last updated date
207 $last_updated_formatted = '';
208 if (!empty($data['last_updated'])) {
209 $last_updated_formatted = self::format_last_updated($data['last_updated']);
210 }
211
212 $formatted_plugins[] = [
213 'name' => self::decode_api_text($data['name'] ?? ''),
214 'slug' => $data['slug'] ?? '',
215 'description' => self::decode_api_text($data['description'] ?? ''),
216 'logo' => $data['logo'] ?? '',
217 'rating' => $data['rating'] ?? 0,
218 'rating_percentage' => $data['rating_percentage'] ?? 0,
219 'num_ratings' => $data['num_ratings'] ?? 0,
220 'active_installs' => $data['active_installs'] ?? '0',
221 'active_installs_count' => $data['active_installs_count'] ?? 0,
222 'downloaded' => $data['downloaded'] ?? 0,
223 'downloaded_formatted' => $data['downloaded_formatted'] ?? '',
224 'version' => $data['version'] ?? '',
225 'tested' => $data['tested'] ?? '',
226 'last_updated' => $data['last_updated'] ?? '',
227 'last_updated_formatted' => $last_updated_formatted,
228 'homepage' => $data['homepage'] ?? '',
229 'status' => $plugin_status,
230 'plugin_file' => $plugin_file,
231 'activate_nonce' => $plugin_file ? wp_create_nonce('activate-plugin_' . $plugin_file) : '',
232 'recommended' => !empty($recommended_by_slug[$slug])
233 ];
234 }
235
236 wp_send_json_success([
237 'plugins' => $formatted_plugins,
238 'loading' => false,
239 'message' => __('Plugin data loaded successfully.', 'ultimate-post-kit')
240 ]);
241 }
242
243 /**
244 * Decode display text coming from the WordPress.org plugins API.
245 *
246 * The API returns strings that are already HTML-encoded, e.g.
247 * "Element Pack Lite &#8211; Addons for Elementor". The renderer escapes
248 * again before injecting into the DOM, which turns the leading "&" into
249 * "&amp;" and prints the entity literally instead of an en dash. Decoding
250 * here means exactly one round of escaping happens, at output.
251 *
252 * Applied when building the response rather than when caching, so
253 * already-cached entries are corrected without waiting for the transient
254 * to expire.
255 *
256 * @param mixed $text Raw value from the API.
257 * @return string Plain text, still to be escaped at output.
258 */
259 private static function decode_api_text($text) {
260 if (!is_string($text) || '' === $text) {
261 return '';
262 }
263
264 return html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
265 }
266
267 /**
268 * Schedule the cron job on init
269 */
270 public static function schedule_cron() {
271 // Make sure the cron hook is registered
272 if (!wp_next_scheduled(self::CRON_HOOK)) {
273 // Don't schedule immediately, only when needed
274 }
275 }
276
277 /**
278 * Get plugin status by slug
279 *
280 * @param string $slug Plugin slug
281 * @return string Plugin status: 'active', 'installed', 'not_installed'
282 */
283 private static function get_plugin_status_by_slug($slug) {
284 if (!function_exists('is_plugin_active')) {
285 require_once ABSPATH . 'wp-admin/includes/plugin.php';
286 }
287
288 // Get all installed plugins
289 $installed_plugins = get_plugins();
290
291 // Find the plugin file for this slug
292 $plugin_file = self::get_plugin_file_by_slug($slug);
293
294 if ($plugin_file && is_plugin_active($plugin_file)) {
295 return 'active';
296 } elseif ($plugin_file && isset($installed_plugins[$plugin_file])) {
297 return 'installed';
298 }
299
300 return 'not_installed';
301 }
302
303 /**
304 * Get plugin file path by slug
305 *
306 * @param string $slug Plugin slug
307 * @return string|null Plugin file path or null if not found
308 */
309 private static function get_plugin_file_by_slug($slug) {
310 if (!function_exists('get_plugins')) {
311 require_once ABSPATH . 'wp-admin/includes/plugin.php';
312 }
313
314 $installed_plugins = get_plugins();
315
316 // Look for the plugin file that matches the slug
317 foreach ($installed_plugins as $plugin_file => $plugin_data) {
318 $plugin_slug = dirname($plugin_file);
319 if ($plugin_slug === $slug) {
320 return $plugin_file;
321 }
322 }
323
324 return null;
325 }
326
327 /**
328 * Format date in human-readable format
329 *
330 * @param string $date_string Date string to format
331 * @return string Formatted date string
332 */
333 private static function format_last_updated($date_string) {
334 if (empty($date_string)) {
335 return __('Unknown', 'ultimate-post-kit');
336 }
337
338 $date = strtotime($date_string);
339 if (!$date) {
340 return __('Unknown', 'ultimate-post-kit');
341 }
342
343 $diff = current_time('timestamp') - $date;
344
345 if ($diff < 60) {
346 return __('Just now', 'ultimate-post-kit');
347 } elseif ($diff < 3600) {
348 $minutes = floor($diff / 60);
349 /* translators: %d: number of minutes */
350 return sprintf(_n('%d minute ago', '%d minutes ago', $minutes, 'ultimate-post-kit'), $minutes);
351 } elseif ($diff < 86400) {
352 $hours = floor($diff / 3600);
353 /* translators: %d: number of hours */
354 return sprintf(_n('%d hour ago', '%d hours ago', $hours, 'ultimate-post-kit'), $hours);
355 } elseif ($diff < 2592000) { // 30 days
356 $days = floor($diff / 86400);
357 /* translators: %d: number of days */
358 return sprintf(_n('%d day ago', '%d days ago', $days, 'ultimate-post-kit'), $days);
359 } elseif ($diff < 31536000) { // 1 year
360 $months = floor($diff / 2592000);
361 /* translators: %d: number of months */
362 return sprintf(_n('%d month ago', '%d months ago', $months, 'ultimate-post-kit'), $months);
363 } else {
364 $years = floor($diff / 31536000);
365 /* translators: %d: number of years */
366 return sprintf(_n('%d year ago', '%d years ago', $years, 'ultimate-post-kit'), $years);
367 }
368 }
369
370 /**
371 * Fetch plugin data from WordPress.org API
372 *
373 * @param string $plugin_slug Plugin slug
374 * @return array|false Plugin data or false on failure
375 */
376 private static function fetch_plugin_from_api($plugin_slug) {
377 $api_url = add_query_arg([
378 'action' => 'plugin_information',
379 'request' => [
380 'slug' => $plugin_slug,
381 'fields' => [
382 'icons' => true,
383 'short_description' => true,
384 'active_installs' => true,
385 'rating' => true,
386 'num_ratings' => true,
387 'downloaded' => true,
388 'last_updated' => true,
389 'homepage' => true,
390 'tested' => true,
391 'requires' => true,
392 'requires_php' => true,
393 'sections' => false,
394 'compatibility' => false,
395 'banners' => false,
396 'contributors' => false,
397 'tags' => false,
398 'reviews' => false,
399 'versions' => false,
400 'installation' => false,
401 'faq' => false,
402 'changelog' => false,
403 'screenshots' => false,
404 'donate_link' => false,
405 ]
406 ]
407 ], 'https://api.wordpress.org/plugins/info/1.2/');
408
409 // Security: Use wp_safe_remote_get instead of wp_remote_get
410 $response = wp_safe_remote_get($api_url, [
411 'timeout' => 30,
412 'user-agent' => 'Ultimate Post Kit Setup Wizard'
413 ]);
414
415 if (is_wp_error($response)) {
416 return false;
417 }
418
419 $body = wp_remote_retrieve_body($response);
420 $data = json_decode($body, true);
421
422 if (empty($data) || !is_array($data)) {
423 return false;
424 }
425
426 $formatted_data = self::format_plugin_data($data);
427
428 if (empty($formatted_data['name']) && empty($formatted_data['slug'])) {
429 return false;
430 }
431
432 return $formatted_data;
433 }
434
435 /**
436 * Format plugin data for our use
437 *
438 * @param array $raw_data Raw API data
439 * @return array Formatted plugin data
440 */
441 private static function format_plugin_data($raw_data) {
442 // Get the best available icon with validation
443 $icon_url = self::get_valid_plugin_icon($raw_data['icons'] ?? []);
444
445 // Format active installs with null safety and real data
446 $active_installs_raw = $raw_data['active_installs'] ?? 0;
447 $active_installs = self::format_active_installs($active_installs_raw);
448 $active_installs_count = self::get_numeric_active_installs($active_installs_raw);
449
450 // Calculate rating percentage with null safety and real data
451 $rating_percentage = 0;
452 $rating_raw = $raw_data['rating'] ?? 0;
453 $num_ratings_raw = $raw_data['num_ratings'] ?? 0;
454
455 if (!empty($rating_raw) && !empty($num_ratings_raw)) {
456 $rating_percentage = ($rating_raw / 100) * 5; // Convert to 5-star scale
457 }
458
459 // Get downloaded count for additional metrics
460 $downloaded_count = $raw_data['downloaded'] ?? 0;
461
462 return [
463 'name' => $raw_data['name'] ?? '',
464 'slug' => $raw_data['slug'] ?? '',
465 'logo' => $icon_url,
466 'description' => $raw_data['short_description'] ?? '',
467 'active_installs' => $active_installs,
468 'active_installs_count' => $active_installs_count,
469 'rating' => round($rating_percentage, 1),
470 'rating_percentage' => $rating_raw,
471 'num_ratings' => $num_ratings_raw,
472 'downloaded' => $downloaded_count,
473 'downloaded_formatted' => self::format_downloaded_count($downloaded_count),
474 'last_updated' => $raw_data['last_updated'] ?? '',
475 'homepage' => $raw_data['homepage'] ?? '',
476 'version' => $raw_data['version'] ?? '',
477 'tested' => $raw_data['tested'] ?? '',
478 'requires' => $raw_data['requires'] ?? '',
479 'requires_php' => $raw_data['requires_php'] ?? '',
480 'fetched_at' => current_time('timestamp')
481 ];
482 }
483
484 /**
485 * Get valid plugin icon with format validation
486 *
487 * @param array $icons Array of icon URLs
488 * @return string Valid icon URL or empty string
489 */
490 private static function get_valid_plugin_icon($icons) {
491 $valid_extensions = ['gif', 'png', 'jpg', 'jpeg', 'svg'];
492 $icon_sizes = ['256', '128', 'default'];
493
494 foreach ($icon_sizes as $size) {
495 if (!empty($icons[$size])) {
496 $icon_url = $icons[$size];
497
498 // Check if URL is valid and has correct extension
499 if (self::is_valid_image_url($icon_url, $valid_extensions)) {
500 return $icon_url;
501 }
502 }
503 }
504
505 return '';
506 }
507
508 /**
509 * Validate image URL and extension
510 *
511 * @param string $url Image URL
512 * @param array $valid_extensions Allowed extensions
513 * @return bool True if valid
514 */
515 private static function is_valid_image_url($url, $valid_extensions) {
516 if (empty($url) || !is_string($url)) {
517 return false;
518 }
519
520 // Check if URL is valid
521 if (!filter_var($url, FILTER_VALIDATE_URL)) {
522 return false;
523 }
524
525 // Get file extension
526 $path_info = pathinfo(wp_parse_url($url, PHP_URL_PATH));
527 $extension = strtolower($path_info['extension'] ?? '');
528
529 return in_array($extension, $valid_extensions);
530 }
531
532 /**
533 * Format active installs number with null safety
534 *
535 * @param mixed $installs Number of active installs
536 * @return string Formatted installs string
537 */
538 private static function format_active_installs($installs) {
539 // Handle null, empty, or non-numeric values
540 if (is_null($installs) || $installs === '' || !is_numeric($installs)) {
541 return '0';
542 }
543
544 $installs = intval($installs);
545
546 if ($installs >= 1000000) {
547 return round($installs / 1000000, 1) . 'M+';
548 } elseif ($installs >= 1000) {
549 return round($installs / 1000, 1) . 'K+';
550 } else {
551 return number_format($installs);
552 }
553 }
554
555 /**
556 * Get numeric active installs count
557 *
558 * @param mixed $installs Number of active installs
559 * @return int Numeric installs count
560 */
561 private static function get_numeric_active_installs($installs) {
562 // Handle null, empty, or non-numeric values
563 if (is_null($installs) || $installs === '' || !is_numeric($installs)) {
564 return 0;
565 }
566
567 return intval($installs);
568 }
569
570 /**
571 * Format downloaded count
572 *
573 * @param mixed $downloaded Number of downloads
574 * @return string Formatted downloads string
575 */
576 private static function format_downloaded_count($downloaded) {
577 // Handle null, empty, or non-numeric values
578 if (is_null($downloaded) || $downloaded === '' || !is_numeric($downloaded)) {
579 return '0';
580 }
581
582 $downloaded = intval($downloaded);
583
584 if ($downloaded >= 1000000) {
585 return round($downloaded / 1000000, 1) . 'M+';
586 } elseif ($downloaded >= 1000) {
587 return round($downloaded / 1000, 1) . 'K+';
588 } else {
589 return number_format($downloaded);
590 }
591 }
592 }
593
594 // Initialize the handler
595 add_action('init', function() {
596 Remote_Data_Handler::init();
597 });
598
599 // Global functions for backward compatibility and ease of use
600 if (!function_exists('upk_is_element_pack_page')) {
601 function upk_is_element_pack_page() {
602 return \UltimatePostKit\SetupWizard\Remote_Data_Handler::is_element_pack_page();
603 }
604 }
605
606 if (!function_exists('upk_get_remote_plugins')) {
607 function upk_get_remote_plugins() {
608 return \UltimatePostKit\SetupWizard\Remote_Data_Handler::get_remote_plugins();
609 }
610 }
611
612 if (!function_exists('upk_schedule_remote_fetch')) {
613 function upk_schedule_remote_fetch() {
614 return \UltimatePostKit\SetupWizard\Remote_Data_Handler::schedule_remote_fetch();
615 }
616 }
617
618 if (!function_exists('upk_fetch_remote_plugins_now')) {
619 function upk_fetch_remote_plugins_now() {
620 return \UltimatePostKit\SetupWizard\Remote_Data_Handler::fetch_remote_plugins_now();
621 }
622 }
623