PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.83
51.1.83 51.1.82 51.1.81 51.1.79 51.1.78 51.1.77 51.1.76 51.1.74 51.1.75 51.1.65 51.1.64 51.1.63 trunk 51.1.14 51.1.2 51.1.35 51.1.36 51.1.37 51.1.38 51.1.39 51.1.44 51.1.45 51.1.46 51.1.47 51.1.49 All 37 releases
king-addons / includes / helpers / Dynamic_Posts_Grid_Ajax.php

Dynamic_Posts_Grid_Ajax.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.83, at includes/helpers/Dynamic_Posts_Grid_Ajax.php

565 lines 21.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /** @noinspection SpellCheckingInspection, DuplicatedCode, PhpUnused */
4
5 namespace King_Addons;
6
7 if (!defined('ABSPATH')) {
8 exit; // Exit if accessed directly.
9 }
10
11 /**
12 * Dynamic Posts Grid AJAX Helper
13 *
14 * Handles AJAX requests for filtering, searching, and load more functionality
15 * for the Dynamic Posts Grid widget.
16 */
17 class Dynamic_Posts_Grid_Ajax
18 {
19 private static ?Dynamic_Posts_Grid_Ajax $_instance = null;
20 private static bool $initialized = false;
21
22 /**
23 * Get singleton instance.
24 */
25 public static function get_instance(): Dynamic_Posts_Grid_Ajax
26 {
27 if (is_null(self::$_instance)) {
28 self::$_instance = new self();
29 }
30 return self::$_instance;
31 }
32
33 /**
34 * Constructor.
35 */
36 public function __construct()
37 {
38 // Prevent double initialization
39 if (self::$initialized) {
40 return;
41 }
42 self::$initialized = true;
43
44 $this->init_hooks();
45 }
46
47 /**
48 * Initialize hooks.
49 */
50 private function init_hooks(): void
51 {
52 // AJAX actions for both logged in and non-logged in users
53 add_action('wp_ajax_king_addons_dynamic_posts_grid_filter', [$this, 'handle_filter_request']);
54 add_action('wp_ajax_nopriv_king_addons_dynamic_posts_grid_filter', [$this, 'handle_filter_request']);
55
56 add_action('wp_ajax_king_addons_dynamic_posts_grid_load_more', [$this, 'handle_load_more_request']);
57 add_action('wp_ajax_nopriv_king_addons_dynamic_posts_grid_load_more', [$this, 'handle_load_more_request']);
58
59 // Enqueue AJAX variables
60 add_action('wp_enqueue_scripts', [$this, 'enqueue_ajax_variables']);
61 }
62
63 /**
64 * Enqueue AJAX variables for frontend.
65 */
66 public function enqueue_ajax_variables(): void
67 {
68 wp_localize_script('jquery', 'KingAddonsDynamicPostsGrid', [
69 'ajaxUrl' => admin_url('admin-ajax.php'),
70 'nonce' => wp_create_nonce('king_addons_dynamic_posts_grid_nonce'),
71 ]);
72 }
73
74 /**
75 * Handle filter AJAX request.
76 */
77 public function handle_filter_request(): void
78 {
79 try {
80 // Verify nonce
81 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'king_addons_dynamic_posts_grid_nonce')) {
82 wp_send_json_error(['message' => esc_html__('Security check failed.', 'king-addons')]);
83 }
84
85 // Get and sanitize POST data
86 $widget_id = sanitize_text_field($_POST['widget_id'] ?? '');
87 $posts_per_page = absint($_POST['posts_per_page'] ?? 12);
88 // Handle post_types - it comes as JSON string from JavaScript or as array
89 $post_types_raw = $_POST['post_types'] ?? '["post"]';
90
91 // Debug: log the type and value of post_types_raw
92 // error_log('DEBUG: post_types_raw type: ' . gettype($post_types_raw) . ', value: ' . print_r($post_types_raw, true));
93
94 // Check if it's already an array (PRO version) or needs decoding (Free version)
95 if (is_array($post_types_raw)) {
96 $post_types = $this->sanitize_array($post_types_raw);
97 } else {
98 // Ensure it's a string before decoding
99 if (!is_string($post_types_raw)) {
100 // error_log('DEBUG: post_types_raw is not a string, converting to: ' . json_encode($post_types_raw));
101 $post_types_raw = json_encode($post_types_raw);
102 }
103 $post_types_decoded = json_decode($post_types_raw, true);
104 $post_types = is_array($post_types_decoded) ? $this->sanitize_array($post_types_decoded) : ['post'];
105 }
106 $orderby = sanitize_text_field($_POST['orderby'] ?? 'date');
107 $order = sanitize_text_field($_POST['order'] ?? 'DESC');
108 $filter_taxonomy = sanitize_text_field($_POST['filter_taxonomy'] ?? 'category');
109 $filter_term = sanitize_text_field($_POST['filter_term'] ?? '*');
110 $search_query = sanitize_text_field($_POST['search_query'] ?? '');
111 $page = absint($_POST['page'] ?? 1);
112 $cpt_actions_raw = wp_unslash($_POST['cpt_actions'] ?? '');
113 $cpt_actions = $this->sanitize_cpt_actions($cpt_actions_raw);
114 $show_excerpt = ($_POST['show_excerpt'] ?? '1') === '1';
115
116 // Build query arguments
117 $query_args = $this->build_query_args([
118 'posts_per_page' => $posts_per_page,
119 'post_types' => $post_types,
120 'orderby' => $orderby,
121 'order' => $order,
122 'filter_taxonomy' => $filter_taxonomy,
123 'filter_term' => $filter_term,
124 'search_query' => $search_query,
125 'page' => $page,
126 ]);
127
128 // Execute query
129 $posts_query = new \WP_Query($query_args);
130
131 // Generate posts HTML (use CPT classing when filtering by post_type)
132 $is_cpt_mode = ($filter_taxonomy === 'post_type');
133 $posts_html = $this->generate_posts_html($posts_query, $show_excerpt, $is_cpt_mode, $cpt_actions);
134
135 // Prepare response data
136 $response_data = [
137 'posts_html' => $posts_html,
138 'current_page' => $page,
139 'max_pages' => $posts_query->max_num_pages,
140 'total_posts' => $posts_query->found_posts,
141 'current_count' => $posts_query->post_count,
142 ];
143
144 wp_send_json_success($response_data);
145 } catch (Exception $e) {
146 // error_log('Dynamic Posts Grid Filter AJAX Error: ' . $e->getMessage());
147 // error_log('Stack trace: ' . $e->getTraceAsString());
148 wp_send_json_error(['message' => esc_html__('An error occurred while filtering posts.', 'king-addons')]);
149 }
150 }
151
152 /**
153 * Handle load more AJAX request.
154 */
155 public function handle_load_more_request(): void
156 {
157 try {
158 // Verify nonce
159 if (!wp_verify_nonce($_POST['nonce'] ?? '', 'king_addons_dynamic_posts_grid_nonce')) {
160 wp_send_json_error(['message' => esc_html__('Security check failed.', 'king-addons')]);
161 }
162
163 // Get and sanitize POST data
164 $widget_id = sanitize_text_field($_POST['widget_id'] ?? '');
165 $posts_per_page = absint($_POST['posts_per_page'] ?? 12);
166 // Handle post_types - it comes as JSON string from JavaScript or as array
167 $post_types_raw = $_POST['post_types'] ?? '["post"]';
168
169 // Debug: log the type and value of post_types_raw
170 // error_log('DEBUG: post_types_raw type: ' . gettype($post_types_raw) . ', value: ' . print_r($post_types_raw, true));
171
172 // Check if it's already an array (PRO version) or needs decoding (Free version)
173 if (is_array($post_types_raw)) {
174 $post_types = $this->sanitize_array($post_types_raw);
175 } else {
176 // Ensure it's a string before decoding
177 if (!is_string($post_types_raw)) {
178 // error_log('DEBUG: post_types_raw is not a string, converting to: ' . json_encode($post_types_raw));
179 $post_types_raw = json_encode($post_types_raw);
180 }
181 $post_types_decoded = json_decode($post_types_raw, true);
182 $post_types = is_array($post_types_decoded) ? $this->sanitize_array($post_types_decoded) : ['post'];
183 }
184 $orderby = sanitize_text_field($_POST['orderby'] ?? 'date');
185 $order = sanitize_text_field($_POST['order'] ?? 'DESC');
186 $filter_taxonomy = sanitize_text_field($_POST['filter_taxonomy'] ?? 'category');
187 $filter_term = sanitize_text_field($_POST['filter_term'] ?? '*');
188 $search_query = sanitize_text_field($_POST['search_query'] ?? '');
189 $page = absint($_POST['page'] ?? 1);
190 $cpt_actions_raw = wp_unslash($_POST['cpt_actions'] ?? '');
191 $cpt_actions = $this->sanitize_cpt_actions($cpt_actions_raw);
192 $show_excerpt = ($_POST['show_excerpt'] ?? '1') === '1';
193
194 // Build query arguments for load more (accumulative)
195 $query_args = $this->build_query_args([
196 'posts_per_page' => $posts_per_page,
197 'post_types' => $post_types,
198 'orderby' => $orderby,
199 'order' => $order,
200 'filter_taxonomy' => $filter_taxonomy,
201 'filter_term' => $filter_term,
202 'search_query' => $search_query,
203 'page' => $page,
204 ]);
205
206 // Execute query
207 $posts_query = new \WP_Query($query_args);
208
209 // Generate posts HTML (use CPT classing when filtering by post_type)
210 $is_cpt_mode = ($filter_taxonomy === 'post_type');
211 $posts_html = $this->generate_posts_html($posts_query, $show_excerpt, $is_cpt_mode, $cpt_actions);
212
213 // Calculate total shown posts (previous pages + current page)
214 $total_shown = (($page - 1) * $posts_per_page) + $posts_query->post_count;
215
216 // Prepare response data
217 $response_data = [
218 'posts_html' => $posts_html,
219 'current_page' => $page,
220 'max_pages' => $posts_query->max_num_pages,
221 'total_posts' => $posts_query->found_posts,
222 'current_count' => $total_shown,
223 ];
224
225 wp_send_json_success($response_data);
226 } catch (Exception $e) {
227 // error_log('Dynamic Posts Grid AJAX Error: ' . $e->getMessage());
228 // error_log('Stack trace: ' . $e->getTraceAsString());
229 wp_send_json_error(['message' => esc_html__('An error occurred while loading posts.', 'king-addons')]);
230 }
231 }
232
233 /**
234 * Build WP_Query arguments.
235 *
236 * @param array $params Query parameters.
237 * @return array WP_Query arguments.
238 */
239 private function build_query_args(array $params): array
240 {
241 $query_args = [
242 'post_type' => $params['post_types'],
243 'posts_per_page' => $params['posts_per_page'],
244 'paged' => $params['page'],
245 'orderby' => $params['orderby'],
246 'order' => $params['order'],
247 'post_status' => 'publish',
248 'ignore_sticky_posts' => true,
249 ];
250
251 // Add search query
252 if (!empty($params['search_query'])) {
253 $query_args['s'] = $params['search_query'];
254 }
255
256 // Add taxonomy filter
257 if (!empty($params['filter_term']) && $params['filter_term'] !== '*') {
258 $query_args['tax_query'] = [
259 [
260 'taxonomy' => $params['filter_taxonomy'],
261 'field' => 'slug',
262 'terms' => $params['filter_term'],
263 ]
264 ];
265 }
266
267 // Handle random order
268 if ($params['orderby'] === 'rand') {
269 $query_args['orderby'] = 'rand';
270 unset($query_args['order']); // Order is not applicable for random
271 }
272
273 return $query_args;
274 }
275
276 /**
277 * Generate posts HTML.
278 *
279 * @param \WP_Query $posts_query WP_Query object.
280 * @param bool $show_excerpt Whether to show excerpt.
281 * @return string Generated HTML.
282 */
283 private function generate_posts_html(\WP_Query $posts_query, bool $show_excerpt = true, bool $is_cpt_mode = false, array $cpt_actions = []): string
284 {
285 if (!$posts_query->have_posts()) {
286 return '<div class="king-addons-dpg-no-posts">' . esc_html__('No posts found.', 'king-addons') . '</div>';
287 }
288
289 ob_start();
290
291 while ($posts_query->have_posts()):
292 $posts_query->the_post();
293 $post = get_post();
294 $cta_text = $this->get_cta_text($post);
295
296 if ($is_cpt_mode) {
297 $post_type = $post->post_type;
298 $card_classes = 'king-addons-dpg-cpt-' . $post_type;
299 $post_type_obj = get_post_type_object($post_type);
300 $post_type_display = $post_type_obj ? strtoupper($post_type_obj->label) : strtoupper($post_type);
301 $post_icon = '<i class="far fa-file-alt"></i>';
302 } else {
303 $post_type_display = $this->get_post_type_display($post);
304 $card_classes = $this->get_post_category_color_classes($post);
305 $post_icon = $this->get_post_type_icon($post);
306 }
307 ?>
308
309 <div class="king-addons-dpg-card king-addons-dpg-item <?php echo esc_attr($card_classes); ?>" data-post-id="<?php echo esc_attr($post->ID); ?>"<?php echo $is_cpt_mode ? ' data-post-type="' . esc_attr($post->post_type) . '"' : ''; ?>>
310
311 <!-- Post Type Header -->
312 <div class="king-addons-dpg-header">
313 <div class="king-addons-dpg-icon">
314 <?php echo $post_icon; ?>
315 </div>
316 <div class="king-addons-dpg-label">
317 <?php echo esc_html($post_type_display); ?>
318 </div>
319 </div>
320
321 <!-- Post Content -->
322 <div class="king-addons-dpg-content">
323 <h3 class="king-addons-dpg-title">
324 <a href="<?php echo esc_url(get_permalink()); ?>">
325 <?php echo esc_html(get_the_title()); ?>
326 </a>
327 </h3>
328
329 <?php if ($show_excerpt): ?>
330 <div class="king-addons-dpg-excerpt">
331 <?php echo esc_html(wp_trim_words(get_the_excerpt(), 20, '...')); ?>
332 </div>
333 <?php endif; ?>
334 </div>
335
336 <!-- CTA Button -->
337 <div class="king-addons-dpg-cta">
338 <?php if ($is_cpt_mode) : ?>
339 <?php
340 $pt = $post->post_type;
341 $action_conf = $cpt_actions[$pt] ?? [];
342 $url_field = $action_conf['url_field'] ?? '';
343 $action_type = $action_conf['action_type'] ?? '';
344 $cta_override = $action_conf['cta_text'] ?? '';
345 $meta_url = $url_field ? get_post_meta($post->ID, $url_field, true) : '';
346 ?>
347 <?php
348 // Security fix: Block dangerous protocols like javascript:, data:, vbscript:
349 $is_safe_url = !preg_match('/^(javascript|data|vbscript):/i', $meta_url);
350 ?>
351 <?php if (!empty($meta_url) && !empty($action_type) && $is_safe_url) : ?>
352 <button class="king-addons-dpg-button king-addons-dpg-action-btn"
353 data-action="<?php echo esc_attr($action_type); ?>"
354 data-url="<?php echo esc_url($meta_url); ?>"
355 data-title="<?php echo esc_attr(get_the_title()); ?>">
356 <?php echo esc_html(strtoupper($cta_override ?: 'VIEW')); ?>
357 </button>
358 <?php else: ?>
359 <a href="<?php echo esc_url(get_permalink()); ?>" class="king-addons-dpg-button">
360 <?php echo esc_html($cta_text); ?>
361 </a>
362 <?php endif; ?>
363 <?php else: ?>
364 <a href="<?php echo esc_url(get_permalink()); ?>" class="king-addons-dpg-button">
365 <?php echo esc_html($cta_text); ?>
366 </a>
367 <?php endif; ?>
368 </div>
369
370 </div>
371
372 <?php
373 endwhile;
374
375 wp_reset_postdata();
376
377 return ob_get_clean();
378 }
379
380 /**
381 * Get post category for display.
382 *
383 * @param object $post Post object.
384 * @return string Post category display name.
385 */
386 private function get_post_type_display($post): string
387 {
388 // Get post categories
389 $taxonomy = 'category';
390 $terms = get_the_terms($post->ID, $taxonomy);
391
392 if ($terms && !is_wp_error($terms)) {
393 // Use first category name
394 $term = $terms[0];
395 return strtoupper($term->name);
396 }
397
398 // Fallback to post format/type based display
399 $format = get_post_format($post->ID);
400
401 if ($format === 'video') {
402 return 'VIDEO';
403 } elseif ($format === 'audio') {
404 return 'PUBLICATION';
405 } elseif ($post->post_type === 'page') {
406 return 'WEBSITE';
407 } else {
408 return 'POST';
409 }
410 }
411
412 /**
413 * Get CTA button text (fallback for AJAX context).
414 *
415 * @param object $post Post object.
416 * @return string CTA button text.
417 */
418 private function get_cta_text($post): string
419 {
420 // Simple fallback for AJAX - smart detection only
421 $taxonomy = 'category';
422 $terms = get_the_terms($post->ID, $taxonomy);
423
424 if ($terms && !is_wp_error($terms)) {
425 $term = $terms[0];
426 $slug = strtolower($term->slug);
427
428 // Basic smart mapping for AJAX
429 $smart_cta_map = [
430 'video' => 'WATCH NOW',
431 'videos' => 'WATCH NOW',
432 'website' => 'LEARN MORE',
433 'websites' => 'LEARN MORE',
434 ];
435
436 if (isset($smart_cta_map[$slug])) {
437 return $smart_cta_map[$slug];
438 }
439 }
440
441 return 'READ MORE';
442 }
443
444 /**
445 * Get post category color classes.
446 *
447 * @param object $post Post object.
448 * @return string Color class names.
449 */
450 private function get_post_category_color_classes($post): string
451 {
452 $classes = [];
453 $taxonomy = 'category'; // Default to category for AJAX requests
454
455 // Get post categories/terms
456 $terms = get_the_terms($post->ID, $taxonomy);
457
458 if ($terms && !is_wp_error($terms)) {
459 foreach ($terms as $term) {
460 $classes[] = 'filter-' . $term->slug;
461 $classes[] = 'king-addons-dpg-category-' . $term->slug;
462 }
463 }
464
465 // Add default class if no terms
466 if (empty($classes)) {
467 $classes[] = 'king-addons-dpg-category-default';
468 }
469
470 return implode(' ', $classes);
471 }
472
473 /**
474 * Get post type icon (fallback icons for AJAX context).
475 *
476 * @param object $post Post object.
477 * @return string Icon HTML.
478 */
479 private function get_post_type_icon($post): string
480 {
481 // Get post categories/terms
482 $taxonomy = 'category';
483 $terms = get_the_terms($post->ID, $taxonomy);
484
485 if ($terms && !is_wp_error($terms)) {
486 // Use first category to determine icon type
487 $term = $terms[0];
488
489 // Map common category names to icons
490 $category_icon_map = [
491 'report' => '<i class="far fa-file-alt"></i>',
492 'reports' => '<i class="far fa-file-alt"></i>',
493 'publication' => '<i class="far fa-newspaper"></i>',
494 'publications' => '<i class="far fa-newspaper"></i>',
495 'video' => '<i class="far fa-play-circle"></i>',
496 'videos' => '<i class="far fa-play-circle"></i>',
497 'website' => '<i class="fas fa-globe"></i>',
498 'websites' => '<i class="fas fa-globe"></i>',
499 'news' => '<i class="far fa-newspaper"></i>',
500 'blog' => '<i class="far fa-newspaper"></i>',
501 'article' => '<i class="far fa-newspaper"></i>',
502 'articles' => '<i class="far fa-newspaper"></i>',
503 ];
504
505 $slug = strtolower($term->slug);
506 if (isset($category_icon_map[$slug])) {
507 return $category_icon_map[$slug];
508 }
509 }
510
511 // Fallback based on post format
512 $format = get_post_format($post->ID);
513
514 if ($format === 'video') {
515 return '<i class="far fa-play-circle"></i>';
516 } elseif ($format === 'audio') {
517 return '<i class="far fa-newspaper"></i>';
518 } elseif ($post->post_type === 'page') {
519 return '<i class="fas fa-globe"></i>';
520 } else {
521 return '<i class="far fa-file-alt"></i>';
522 }
523 }
524
525 /**
526 * Sanitize CPT actions configuration JSON.
527 *
528 * @param string $raw_json Raw JSON from request.
529 * @return array Sanitized CPT actions config.
530 */
531 private function sanitize_cpt_actions(string $raw_json): array
532 {
533 if (empty($raw_json)) {
534 return [];
535 }
536 $decoded = json_decode($raw_json, true);
537 if (!is_array($decoded)) {
538 return [];
539 }
540 $sanitized = [];
541 foreach ($decoded as $pt => $conf) {
542 $pt_key = sanitize_key($pt);
543 if (!$pt_key) {
544 continue;
545 }
546 $sanitized[$pt_key] = [
547 'url_field' => isset($conf['url_field']) ? sanitize_key($conf['url_field']) : '',
548 'action_type' => isset($conf['action_type']) ? sanitize_text_field($conf['action_type']) : '',
549 'cta_text' => isset($conf['cta_text']) ? sanitize_text_field($conf['cta_text']) : '',
550 ];
551 }
552 return $sanitized;
553 }
554
555 /**
556 * Sanitize array values.
557 *
558 * @param array $array Array to sanitize.
559 * @return array Sanitized array.
560 */
561 private function sanitize_array(array $array): array
562 {
563 return array_map('sanitize_text_field', $array);
564 }
565 }