| @@ -15,13 +15,33 @@ | ||
| 15 | 15 | * for the Dynamic Posts Grid widget. |
| 16 | 16 | */ |
| 17 | 17 | class Dynamic_Posts_Grid_Ajax |
| 18 | 18 | { |
| 19 | + private static ?Dynamic_Posts_Grid_Ajax $_instance = null; | |
| 20 | + private static bool $initialized = false; | |
| 21 | + | |
| 19 | 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 | + /** | |
| 20 | 34 | * Constructor. |
| 21 | 35 | */ |
| 22 | 36 | public function __construct() |
| 23 | 37 | { |
| 38 | + // Prevent double initialization | |
| 39 | + if (self::$initialized) { | |
| 40 | + return; | |
| 41 | + } | |
| 42 | + self::$initialized = true; | |
| 43 | + | |
| 24 | 44 | $this->init_hooks(); |
| 25 | 45 | } |
| 26 | 46 | |
| 27 | 47 | /** |
| @@ -55,17 +75,35 @@ | ||
| 55 | 75 | * Handle filter AJAX request. |
| 56 | 76 | */ |
| 57 | 77 | public function handle_filter_request(): void |
| 58 | 78 | { |
| 59 | - // Verify nonce | |
| 60 | - if (!wp_verify_nonce($_POST['nonce'] ?? '', 'king_addons_dynamic_posts_grid_nonce')) { | |
| 61 | - wp_send_json_error(['message' => esc_html__('Security check failed.', 'king-addons')]); | |
| 62 | - } | |
| 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 | + } | |
| 63 | 84 | |
| 64 | 85 | // Get and sanitize POST data |
| 65 | 86 | $widget_id = sanitize_text_field($_POST['widget_id'] ?? ''); |
| 66 | 87 | $posts_per_page = absint($_POST['posts_per_page'] ?? 12); |
| 67 | - $post_types = $this->sanitize_array($_POST['post_types'] ?? ['post']); | |
| 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 | + } | |
| 68 | 106 | $orderby = sanitize_text_field($_POST['orderby'] ?? 'date'); |
| 69 | 107 | $order = sanitize_text_field($_POST['order'] ?? 'DESC'); |
| 70 | 108 | $filter_taxonomy = sanitize_text_field($_POST['filter_taxonomy'] ?? 'category'); |
| 71 | 109 | $filter_term = sanitize_text_field($_POST['filter_term'] ?? '*'); |
| @@ -70,8 +108,10 @@ | ||
| 70 | 108 | $filter_taxonomy = sanitize_text_field($_POST['filter_taxonomy'] ?? 'category'); |
| 71 | 109 | $filter_term = sanitize_text_field($_POST['filter_term'] ?? '*'); |
| 72 | 110 | $search_query = sanitize_text_field($_POST['search_query'] ?? ''); |
| 73 | 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); | |
| 74 | 114 | $show_excerpt = ($_POST['show_excerpt'] ?? '1') === '1'; |
| 75 | 115 | |
| 76 | 116 | // Build query arguments |
| 77 | 117 | $query_args = $this->build_query_args([ |
| @@ -87,10 +127,11 @@ | ||
| 87 | 127 | |
| 88 | 128 | // Execute query |
| 89 | 129 | $posts_query = new \WP_Query($query_args); |
| 90 | 130 | |
| 91 | - // Generate posts HTML | |
| 92 | - $posts_html = $this->generate_posts_html($posts_query, $show_excerpt); | |
| 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); | |
| 93 | 134 | |
| 94 | 135 | // Prepare response data |
| 95 | 136 | $response_data = [ |
| 96 | 137 | 'posts_html' => $posts_html, |
| @@ -100,8 +141,13 @@ | ||
| 100 | 141 | 'current_count' => $posts_query->post_count, |
| 101 | 142 | ]; |
| 102 | 143 | |
| 103 | 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 | + } | |
| 104 | 150 | } |
| 105 | 151 | |
| 106 | 152 | /** |
| 107 | 153 | * Handle load more AJAX request. |
| @@ -107,17 +153,35 @@ | ||
| 107 | 153 | * Handle load more AJAX request. |
| 108 | 154 | */ |
| 109 | 155 | public function handle_load_more_request(): void |
| 110 | 156 | { |
| 111 | - // Verify nonce | |
| 112 | - if (!wp_verify_nonce($_POST['nonce'] ?? '', 'king_addons_dynamic_posts_grid_nonce')) { | |
| 113 | - wp_send_json_error(['message' => esc_html__('Security check failed.', 'king-addons')]); | |
| 114 | - } | |
| 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 | + } | |
| 115 | 162 | |
| 116 | 163 | // Get and sanitize POST data |
| 117 | 164 | $widget_id = sanitize_text_field($_POST['widget_id'] ?? ''); |
| 118 | 165 | $posts_per_page = absint($_POST['posts_per_page'] ?? 12); |
| 119 | - $post_types = $this->sanitize_array($_POST['post_types'] ?? ['post']); | |
| 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 | + } | |
| 120 | 184 | $orderby = sanitize_text_field($_POST['orderby'] ?? 'date'); |
| 121 | 185 | $order = sanitize_text_field($_POST['order'] ?? 'DESC'); |
| 122 | 186 | $filter_taxonomy = sanitize_text_field($_POST['filter_taxonomy'] ?? 'category'); |
| 123 | 187 | $filter_term = sanitize_text_field($_POST['filter_term'] ?? '*'); |
| @@ -122,8 +186,10 @@ | ||
| 122 | 186 | $filter_taxonomy = sanitize_text_field($_POST['filter_taxonomy'] ?? 'category'); |
| 123 | 187 | $filter_term = sanitize_text_field($_POST['filter_term'] ?? '*'); |
| 124 | 188 | $search_query = sanitize_text_field($_POST['search_query'] ?? ''); |
| 125 | 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); | |
| 126 | 192 | $show_excerpt = ($_POST['show_excerpt'] ?? '1') === '1'; |
| 127 | 193 | |
| 128 | 194 | // Build query arguments for load more (accumulative) |
| 129 | 195 | $query_args = $this->build_query_args([ |
| @@ -139,10 +205,11 @@ | ||
| 139 | 205 | |
| 140 | 206 | // Execute query |
| 141 | 207 | $posts_query = new \WP_Query($query_args); |
| 142 | 208 | |
| 143 | - // Generate posts HTML | |
| 144 | - $posts_html = $this->generate_posts_html($posts_query, $show_excerpt); | |
| 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); | |
| 145 | 212 | |
| 146 | 213 | // Calculate total shown posts (previous pages + current page) |
| 147 | 214 | $total_shown = (($page - 1) * $posts_per_page) + $posts_query->post_count; |
| 148 | 215 | |
| @@ -155,8 +222,13 @@ | ||
| 155 | 222 | 'current_count' => $total_shown, |
| 156 | 223 | ]; |
| 157 | 224 | |
| 158 | 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 | + } | |
| 159 | 231 | } |
| 160 | 232 | |
| 161 | 233 | /** |
| 162 | 234 | * Build WP_Query arguments. |
| @@ -207,9 +279,9 @@ | ||
| 207 | 279 | * @param \WP_Query $posts_query WP_Query object. |
| 208 | 280 | * @param bool $show_excerpt Whether to show excerpt. |
| 209 | 281 | * @return string Generated HTML. |
| 210 | 282 | */ |
| 211 | - private function generate_posts_html(\WP_Query $posts_query, bool $show_excerpt = true): string | |
| 283 | + private function generate_posts_html(\WP_Query $posts_query, bool $show_excerpt = true, bool $is_cpt_mode = false, array $cpt_actions = []): string | |
| 212 | 284 | { |
| 213 | 285 | if (!$posts_query->have_posts()) { |
| 214 | 286 | return '<div class="king-addons-dpg-no-posts">' . esc_html__('No posts found.', 'king-addons') . '</div>'; |
| 215 | 287 | } |
| @@ -218,15 +290,24 @@ | ||
| 218 | 290 | |
| 219 | 291 | while ($posts_query->have_posts()): |
| 220 | 292 | $posts_query->the_post(); |
| 221 | 293 | $post = get_post(); |
| 222 | - $post_type_display = $this->get_post_type_display($post); | |
| 223 | - $category_classes = $this->get_post_category_color_classes($post); | |
| 224 | 294 | $cta_text = $this->get_cta_text($post); |
| 225 | - $post_icon = $this->get_post_type_icon($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 | + } | |
| 226 | 307 | ?> |
| 227 | 308 | |
| 228 | - <div class="king-addons-dpg-card king-addons-dpg-item <?php echo esc_attr($category_classes); ?>" data-post-id="<?php echo esc_attr($post->ID); ?>"> | |
| 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) . '"' : ''; ?>> | |
| 229 | 310 | |
| 230 | 311 | <!-- Post Type Header --> |
| 231 | 312 | <div class="king-addons-dpg-header"> |
| 232 | 313 | <div class="king-addons-dpg-icon"> |
| @@ -253,11 +334,38 @@ | ||
| 253 | 334 | </div> |
| 254 | 335 | |
| 255 | 336 | <!-- CTA Button --> |
| 256 | 337 | <div class="king-addons-dpg-cta"> |
| 257 | - <a href="<?php echo esc_url(get_permalink()); ?>" class="king-addons-dpg-button"> | |
| 258 | - <?php echo esc_html($cta_text); ?> | |
| 259 | - </a> | |
| 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; ?> | |
| 260 | 368 | </div> |
| 261 | 369 | |
| 262 | 370 | </div> |
| 263 | 371 | |
| @@ -411,8 +519,38 @@ | ||
| 411 | 519 | return '<i class="fas fa-globe"></i>'; |
| 412 | 520 | } else { |
| 413 | 521 | return '<i class="far fa-file-alt"></i>'; |
| 414 | 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; | |
| 415 | 553 | } |
| 416 | 554 | |
| 417 | 555 | /** |
| 418 | 556 | * Sanitize array values. |