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

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