PluginProbe
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets / 4.2.0
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets v4.2.0
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.2.0, at includes/setup-wizard/class-remote-data-handler.php

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