PluginProbe
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder / 51.1.78
King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder v51.1.78
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 / extensions / Template_Catalog_Button / Template_Catalog_Button.php

Template_Catalog_Button.php in King Addons for Elementor – 80+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce, Mega Menu, Popup Builder 51.1.78, at includes/extensions/Template_Catalog_Button/Template_Catalog_Button.php

1,009 lines 37.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Template Catalog Button Extension
5 *
6 * Adds a button to Elementor editor panel that opens King Addons template catalog in popup
7 */
8
9 namespace King_Addons;
10
11 if (!defined('ABSPATH')) {
12 exit; // Exit if accessed directly.
13 }
14
15 class Template_Catalog_Button
16 {
17 /**
18 * Instance
19 *
20 * @var Template_Catalog_Button|null The single instance of the class.
21 */
22 private static ?Template_Catalog_Button $_instance = null;
23
24 /**
25 * Instance
26 *
27 * Ensures only one instance of the class is loaded or can be loaded.
28 *
29 * @return Template_Catalog_Button An instance of the class.
30 */
31 public static function instance(): Template_Catalog_Button
32 {
33 if (is_null(self::$_instance)) {
34 self::$_instance = new self();
35 }
36 return self::$_instance;
37 }
38
39 /**
40 * Constructor
41 */
42 public function __construct()
43 {
44 // Only load if templates catalog is enabled
45 if (!KING_ADDONS_EXT_TEMPLATES_CATALOG) {
46 return;
47 }
48
49 // Check if template catalog button is disabled by premium user
50 if ($this->is_template_catalog_disabled()) {
51 return;
52 }
53
54 // Hook into Elementor editor
55 add_action('elementor/editor/before_enqueue_scripts', [$this, 'enqueue_editor_scripts'], 10);
56 add_action('elementor/editor/after_enqueue_styles', [$this, 'enqueue_editor_styles'], 10);
57
58 // AJAX endpoints for template catalog in editor
59 add_action('wp_ajax_king_addons_get_template_catalog', [$this, 'get_template_catalog']);
60 add_action('wp_ajax_king_addons_import_template_to_page', [$this, 'import_template_to_page']);
61 add_action('wp_ajax_king_addons_import_template_content', [$this, 'import_template_content']);
62
63 // New endpoint for merging with existing page
64 add_action('wp_ajax_king_addons_merge_with_existing_page', [$this, 'merge_with_existing_page']);
65
66 // Sections catalog endpoint
67 add_action('wp_ajax_king_addons_get_sections_catalog', [$this, 'get_sections_catalog']);
68
69 // Section import endpoints
70 add_action('wp_ajax_king_addons_import_section_to_page', [$this, 'import_section_to_page']);
71 }
72
73 /**
74 * Enqueue scripts for Elementor editor
75 */
76 public function enqueue_editor_scripts(): void
77 {
78 wp_enqueue_script(
79 'king-addons-template-catalog-button',
80 KING_ADDONS_URL . 'includes/extensions/Template_Catalog_Button/assets/template-catalog-button.js',
81 ['jquery', 'elementor-editor'],
82 KING_ADDONS_VERSION,
83 true
84 );
85
86 // Get current post ID if available
87 $current_post_id = 0;
88 if (isset($_GET['post'])) {
89 $current_post_id = intval($_GET['post']);
90 } elseif (isset($_GET['post_id'])) {
91 $current_post_id = intval($_GET['post_id']);
92 }
93
94 // Check if this is a Woo Builder template - don't show "Start with a Template" for them.
95 // We detect by our own meta as well as Elementor's template type, because on some flows
96 // ka_woo_template_type may not be saved yet when the editor first loads.
97 $is_woo_builder_template = false;
98 if ($current_post_id > 0) {
99 $post_type = get_post_type($current_post_id);
100 if ('elementor_library' === $post_type) {
101 $woo_template_type = get_post_meta($current_post_id, 'ka_woo_template_type', true);
102 $elementor_template_type = get_post_meta($current_post_id, '_elementor_template_type', true);
103
104 if (!empty($woo_template_type) || 'king-addons-woo-builder' === $elementor_template_type) {
105 $is_woo_builder_template = true;
106 }
107 }
108 }
109
110 // Localize script with template catalog data
111 wp_localize_script(
112 'king-addons-template-catalog-button',
113 'kingAddonsTemplateCatalog',
114 [
115 'templateCatalogUrl' => admin_url('admin.php?page=king-addons-templates'),
116 'templatesEnabled' => KING_ADDONS_EXT_TEMPLATES_CATALOG,
117 'buttonEnabled' => !$this->is_template_catalog_disabled() && !$is_woo_builder_template,
118 'isWooBuilderTemplate' => $is_woo_builder_template,
119 'buttonText' => $this->get_button_text(),
120 'nonce' => wp_create_nonce('king_addons_template_catalog'),
121 'ajaxUrl' => admin_url('admin-ajax.php'),
122 'isPremium' => function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code(),
123 'currentPostId' => $current_post_id,
124 'adminUrl' => admin_url(),
125 'pluginUrl' => KING_ADDONS_URL,
126 ]
127 );
128 }
129
130 /**
131 * Enqueue styles for Elementor editor
132 */
133 public function enqueue_editor_styles(): void
134 {
135 wp_enqueue_style(
136 'king-addons-template-catalog-popup',
137 KING_ADDONS_URL . 'includes/extensions/Template_Catalog_Button/assets/template-catalog-popup.css',
138 [],
139 KING_ADDONS_VERSION
140 );
141 }
142
143 /**
144 * Check if template catalog button is disabled by premium user
145 */
146 private function is_template_catalog_disabled(): bool
147 {
148 // Only premium users can disable the template catalog button
149 if (!function_exists('king_addons_freemius') || !king_addons_freemius()->can_use_premium_code()) {
150 return false;
151 }
152
153 // Check if setting exists and is enabled (1 = disabled)
154 $disabled = get_option('king_addons_disable_template_catalog_button', '0');
155 return $disabled === '1';
156 }
157
158 /**
159 * Get button text based on user's subscription level
160 */
161 private function get_button_text(): string
162 {
163 if (function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code()) {
164 return esc_html__('Templates Pro', 'king-addons');
165 }
166
167 return esc_html__('Free Templates', 'king-addons');
168 }
169
170 /**
171 * AJAX handler for getting template catalog data
172 */
173 public function get_template_catalog(): void
174 {
175 if (!current_user_can('edit_posts')) {
176 wp_send_json_error('Insufficient permissions');
177 return;
178 }
179
180 if (!wp_verify_nonce($_POST['nonce'], 'king_addons_template_catalog')) {
181 wp_send_json_error('Invalid nonce');
182 return;
183 }
184
185 $templates = \King_Addons\TemplatesMap::getTemplatesMapArray();
186 $collections = \King_Addons\CollectionsMap::getCollectionsMapArray();
187
188 $is_premium_active = function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code();
189
190 // Get filters from request
191 $search_query = isset($_POST['search']) ? sanitize_text_field($_POST['search']) : '';
192 $selected_category = isset($_POST['category']) ? sanitize_text_field($_POST['category']) : '';
193 $selected_collection = isset($_POST['collection']) ? sanitize_text_field($_POST['collection']) : '';
194 $current_page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : 1;
195
196 // Get categories and tags
197 $categories = [];
198 $tags = [];
199 $category_counts = [];
200
201 foreach ($templates['templates'] as $template) {
202 if (!in_array($template['category'], $categories)) {
203 $categories[] = $template['category'];
204 }
205
206 foreach ($template['tags'] as $tag) {
207 if (!in_array($tag, $tags)) {
208 $tags[] = $tag;
209 }
210 }
211
212 $category = $template['category'];
213 $category_counts[$category] = isset($category_counts[$category]) ? $category_counts[$category] + 1 : 1;
214 }
215
216 sort($categories);
217
218 // Filter templates
219 $filtered_templates = $templates['templates'];
220
221 // Apply filters
222 if (!empty($search_query)) {
223 $matched_by_title = [];
224 $matched_by_tags = [];
225
226 foreach ($filtered_templates as $template_key => $template) {
227 $found_in_title = stripos($template['title'], $search_query) !== false;
228 $found_in_tags = false;
229
230 foreach ($template['tags'] as $tag) {
231 if (stripos($tag, $search_query) !== false) {
232 $found_in_tags = true;
233 break;
234 }
235 }
236
237 if ($found_in_title) {
238 $template['template_key'] = $template_key;
239 $matched_by_title[] = $template;
240 } elseif ($found_in_tags) {
241 $template['template_key'] = $template_key;
242 $matched_by_tags[] = $template;
243 }
244 }
245
246 $filtered_templates = array_merge($matched_by_title, $matched_by_tags);
247 } else {
248 // Add template keys for non-search results
249 $temp_templates = [];
250 foreach ($filtered_templates as $key => $template) {
251 $template['template_key'] = $key;
252 $temp_templates[] = $template;
253 }
254 $filtered_templates = $temp_templates;
255 }
256
257 if (!empty($selected_category)) {
258 $filtered_templates = array_filter($filtered_templates, function($template) use ($selected_category) {
259 return $template['category'] === $selected_category;
260 });
261 }
262
263 if (!empty($selected_collection)) {
264 $filtered_templates = array_filter($filtered_templates, function($template) use ($selected_collection) {
265 return $template['collection'] == $selected_collection;
266 });
267 }
268
269 // Pagination
270 $items_per_page = 20;
271 $total_templates = count($filtered_templates);
272 $total_pages = ceil($total_templates / $items_per_page);
273 $offset = ($current_page - 1) * $items_per_page;
274 $paged_templates = array_slice($filtered_templates, $offset, $items_per_page);
275
276 wp_send_json_success([
277 'templates' => $paged_templates,
278 'categories' => $categories,
279 'collections' => $collections,
280 'category_counts' => $category_counts,
281 'pagination' => [
282 'current_page' => $current_page,
283 'total_pages' => $total_pages,
284 'total_templates' => $total_templates,
285 'items_per_page' => $items_per_page
286 ],
287 'is_premium_active' => $is_premium_active
288 ]);
289 }
290
291 /**
292 * AJAX handler for importing template to current page
293 */
294 public function import_template_to_page(): void
295 {
296 if (!current_user_can('edit_posts')) {
297 wp_send_json_error('Insufficient permissions');
298 return;
299 }
300
301 if (!wp_verify_nonce($_POST['nonce'], 'king_addons_template_catalog')) {
302 wp_send_json_error('Invalid nonce');
303 return;
304 }
305
306 $template_key = sanitize_text_field($_POST['template_key']);
307 $template_plan = sanitize_text_field($_POST['template_plan']);
308 $is_premium_active = function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code();
309
310 // Determine API URL and install ID
311 if ($is_premium_active && $template_plan === 'premium') {
312 $api_url = 'https://api.kingaddons.com/get-template.php';
313
314 // Use the same method as original templates catalog
315 if (function_exists('king_addons_freemius')) {
316 $freemius_site = king_addons_freemius()->get_site();
317 $install_id = $freemius_site ? $freemius_site->id : 0;
318 } else {
319 $install_id = 0;
320 }
321
322 // error_log('King Addons Premium Template: Using install_id: ' . $install_id . ' for premium template: ' . $template_key);
323 } elseif ($template_plan === 'free') {
324 $api_url = 'https://api.kingaddons.com/get-template-free.php';
325 $install_id = 0;
326 // error_log('King Addons Free Template: Fetching free template: ' . $template_key);
327 } else {
328 // error_log('King Addons Template Error: Premium template requires premium license. Template: ' . $template_key . ', Plan: ' . $template_plan . ', Premium Active: ' . ($is_premium_active ? 'Yes' : 'No'));
329 wp_send_json_error('Premium template requires premium license');
330 return;
331 }
332
333 // Get template data from API
334 $response = wp_remote_post($api_url, [
335 'headers' => ['Content-Type' => 'application/json'],
336 'body' => json_encode([
337 'key' => $template_key,
338 'install' => $install_id,
339 ]),
340 'timeout' => 60
341 ]);
342
343 if (is_wp_error($response)) {
344 wp_send_json_error('Failed to fetch template: ' . $response->get_error_message());
345 return;
346 }
347
348 $body = wp_remote_retrieve_body($response);
349 $data = json_decode($body, true);
350
351 // error_log('King Addons API Response: ' . substr($body, 0, 500) . (strlen($body) > 500 ? '...' : ''));
352
353 if (!$data) {
354 // error_log('King Addons Template Error: Failed to decode JSON response');
355 wp_send_json_error('Invalid JSON response from template API');
356 return;
357 }
358
359 if (!isset($data['success']) || !$data['success']) {
360 $error_message = isset($data['message']) ? $data['message'] : 'Unknown API error';
361 // error_log('King Addons Template Error: API returned error: ' . $error_message);
362 wp_send_json_error('Template API error: ' . $error_message);
363 return;
364 }
365
366 // Return template data for frontend processing
367 wp_send_json_success([
368 'template_data' => $data['landing'],
369 'message' => 'Template data retrieved successfully'
370 ]);
371 }
372
373 /**
374 * AJAX handler for importing template content directly into current page
375 */
376 public function import_template_content(): void
377 {
378 if (!current_user_can('edit_posts')) {
379 wp_send_json_error('Insufficient permissions');
380 return;
381 }
382
383 if (!wp_verify_nonce($_POST['nonce'], 'king_addons_template_catalog')) {
384 wp_send_json_error('Invalid nonce');
385 return;
386 }
387
388 // Security fix: Sanitize template data input
389 $raw_template_data = sanitize_textarea_field(stripslashes($_POST['template_data'] ?? ''));
390 $template_data = json_decode($raw_template_data, true);
391 $page_id = intval($_POST['page_id'] ?? 0);
392
393 // error_log('King Addons Template Import: Starting import for page ID: ' . $page_id);
394 // error_log('King Addons Template Import: Template data keys: ' . json_encode(array_keys($template_data ?: [])));
395
396 if (!$template_data || !$page_id) {
397 // error_log('King Addons Template Import: Invalid data - template_data: ' . (!empty($template_data) ? 'valid' : 'invalid') . ', page_id: ' . $page_id);
398 wp_send_json_error('Invalid template data or page ID');
399 return;
400 }
401
402 // Get current page Elementor data
403 $current_data = get_post_meta($page_id, '_elementor_data', true);
404 $current_elements = json_decode($current_data, true);
405
406 if (!is_array($current_elements)) {
407 $current_elements = [];
408 }
409
410 // Parse template content
411 $template_content = isset($template_data['content']) ? $template_data['content'] : null;
412 if (!$template_content) {
413 wp_send_json_error('No template content found');
414 return;
415 }
416
417 // If template_content is a string, decode it
418 if (is_string($template_content)) {
419 $template_content = json_decode($template_content, true);
420 }
421
422 if (!is_array($template_content)) {
423 wp_send_json_error('Invalid template content format');
424 return;
425 }
426
427 // Process images in template content
428 $image_map = [];
429 $images_processed = 0;
430 $images_failed = 0;
431
432 if (isset($template_data['images']) && is_array($template_data['images'])) {
433 // error_log('King Addons Template Import: Processing ' . count($template_data['images']) . ' images');
434
435 foreach ($template_data['images'] as $image) {
436 // Download and import image
437 $new_image_id = $this->download_and_import_image($image['url']);
438 if ($new_image_id) {
439 $image_map[$image['id']] = $new_image_id;
440 $images_processed++;
441 // error_log('King Addons Template Import: Successfully imported image ' . $image['url'] . ' as ID ' . $new_image_id);
442 } else {
443 $images_failed++;
444 // error_log('King Addons Template Import: Failed to import image ' . $image['url']);
445 }
446 }
447
448 // error_log('King Addons Template Import: Images summary - processed: ' . $images_processed . ', failed: ' . $images_failed);
449 } else {
450 // error_log('King Addons Template Import: No images to process');
451 }
452
453 // Replace image IDs in template content
454 $template_content = $this->replace_image_ids($template_content, $image_map);
455
456 // Merge template content with current page content
457 $current_count = count($current_elements);
458 $new_count = count($template_content);
459 $merged_elements = array_merge($current_elements, $template_content);
460 $total_count = count($merged_elements);
461
462 // error_log('King Addons Template Import: Merging content - current: ' . $current_count . ', new: ' . $new_count . ', total: ' . $total_count);
463
464 // Update page meta
465 $update_result = update_post_meta($page_id, '_elementor_data', wp_slash(json_encode($merged_elements)));
466 update_post_meta($page_id, '_elementor_edit_mode', 'builder');
467
468 // error_log('King Addons Template Import: Page meta updated - result: ' . ($update_result ? 'success' : 'failed'));
469
470 // Clear Elementor cache
471 if (class_exists('\Elementor\Plugin')) {
472 \Elementor\Plugin::$instance->files_manager->clear_cache();
473 // error_log('King Addons Template Import: Elementor cache cleared');
474 } else {
475 // error_log('King Addons Template Import: Elementor Plugin class not found, cache not cleared');
476 }
477
478 // error_log('King Addons Template Import: Import completed successfully');
479
480 wp_send_json_success([
481 'message' => 'Template imported successfully',
482 'imported_elements' => $new_count,
483 'images_processed' => $images_processed,
484 'images_failed' => $images_failed,
485 'page_id' => $page_id,
486 'current_elements_before' => $current_count,
487 'total_elements_after' => $total_count
488 ]);
489 }
490
491 /**
492 * Download and import image to WordPress media library
493 */
494 private function download_and_import_image($image_url): ?int
495 {
496 try {
497 // Security fix: Validate URL to prevent SSRF attacks
498 if (!$this->is_safe_image_url($image_url)) {
499 // error_log('King Addons Security: Blocked unsafe image URL: ' . $image_url);
500 return null;
501 }
502
503 $response = wp_remote_get($image_url, [
504 'timeout' => 30,
505 'user-agent' => 'King Addons Template Import/1.0',
506 'redirection' => 2 // Limit redirects
507 ]);
508
509 if (is_wp_error($response)) {
510 return null;
511 }
512
513 $status_code = wp_remote_retrieve_response_code($response);
514 if ($status_code !== 200) {
515 return null;
516 }
517
518 $image_data = wp_remote_retrieve_body($response);
519 if (empty($image_data)) {
520 return null;
521 }
522
523 // Security fix: Sanitize filename components
524 $image_name = sanitize_file_name(pathinfo(basename($image_url), PATHINFO_FILENAME));
525 $image_extension = sanitize_file_name(pathinfo(basename($image_url), PATHINFO_EXTENSION));
526
527 // Validate file extension
528 $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
529 if (!in_array(strtolower($image_extension), $allowed_extensions, true)) {
530 // error_log('King Addons Security: Invalid image extension: ' . $image_extension);
531 return null;
532 }
533
534 $unique_image_name = $image_name . '-' . time() . '.' . $image_extension;
535
536 $upload_dir = wp_upload_dir();
537 if (!file_exists($upload_dir['path'])) {
538 wp_mkdir_p($upload_dir['path']);
539 }
540 $image_file = $upload_dir['path'] . '/' . $unique_image_name;
541
542 if (file_put_contents($image_file, $image_data) === false) {
543 return null;
544 }
545
546 $wp_filetype = wp_check_filetype($unique_image_name);
547 $attachment = [
548 'post_mime_type' => $wp_filetype['type'],
549 'post_title' => sanitize_file_name($unique_image_name),
550 'post_content' => '',
551 'post_status' => 'inherit',
552 ];
553
554 $attach_id = wp_insert_attachment($attachment, $image_file);
555
556 require_once(ABSPATH . 'wp-admin/includes/image.php');
557 $attach_data = wp_generate_attachment_metadata($attach_id, $image_file);
558 wp_update_attachment_metadata($attach_id, $attach_data);
559
560 return $attach_id;
561
562 } catch (\Exception $e) {
563 return null;
564 }
565 }
566
567 /**
568 * Replace image IDs in template content
569 */
570 private function replace_image_ids($content, $image_map): array
571 {
572 if (!is_array($content)) {
573 return $content;
574 }
575
576 foreach ($content as &$element) {
577 if (isset($element['settings'])) {
578 $element['settings'] = $this->replace_image_ids_in_settings($element['settings'], $image_map);
579 }
580
581 if (isset($element['elements']) && is_array($element['elements'])) {
582 $element['elements'] = $this->replace_image_ids($element['elements'], $image_map);
583 }
584 }
585
586 return $content;
587 }
588
589 /**
590 * Generate new unique IDs for all elements to avoid conflicts on repeated imports
591 */
592 private function regenerate_element_ids($content): array
593 {
594 if (!is_array($content)) {
595 return $content;
596 }
597
598 foreach ($content as &$element) {
599 // Generate new unique ID for this element
600 if (isset($element['id'])) {
601 $element['id'] = $this->generate_unique_elementor_id();
602 }
603
604 // Process nested elements recursively
605 if (isset($element['elements']) && is_array($element['elements'])) {
606 $element['elements'] = $this->regenerate_element_ids($element['elements']);
607 }
608 }
609
610 return $content;
611 }
612
613 /**
614 * Generate a unique Elementor-style ID
615 */
616 private function generate_unique_elementor_id(): string
617 {
618 // Elementor uses 7-character alphanumeric IDs
619 $chars = '0123456789abcdef';
620 $id = '';
621 for ($i = 0; $i < 7; $i++) {
622 $id .= $chars[rand(0, strlen($chars) - 1)];
623 }
624 return $id;
625 }
626
627 /**
628 * Replace image IDs in element settings
629 */
630 private function replace_image_ids_in_settings($settings, $image_map): array
631 {
632 if (!is_array($settings)) {
633 return $settings;
634 }
635
636 foreach ($settings as $key => &$value) {
637 if (is_array($value)) {
638 $value = $this->replace_image_ids_in_settings($value, $image_map);
639 } elseif (isset($image_map[$value])) {
640 // Replace image ID
641 $value = $image_map[$value];
642 }
643 }
644
645 return $settings;
646 }
647
648 /**
649 * Merge processed template content with existing page
650 */
651 public function merge_with_existing_page(): void
652 {
653 if (!current_user_can('edit_posts')) {
654 wp_send_json_error('Insufficient permissions');
655 return;
656 }
657
658 if (!wp_verify_nonce($_POST['nonce'], 'king_addons_template_catalog')) {
659 wp_send_json_error('Invalid nonce');
660 return;
661 }
662
663 $page_id = intval($_POST['page_id']);
664
665 if (!$page_id) {
666 wp_send_json_error('Invalid page ID');
667 return;
668 }
669
670 // Get processed content from the original import system
671 $content = get_transient('elementor_import_content');
672 $page_title = get_transient('elementor_import_page_title');
673
674 if (!$content) {
675 wp_send_json_error('No processed content found. Import may have expired.');
676 return;
677 }
678
679 // error_log('King Addons Import: Merging processed content with existing page ' . $page_id);
680
681 // Get current page Elementor data
682 $current_data = get_post_meta($page_id, '_elementor_data', true);
683 $current_elements = json_decode($current_data, true);
684
685 if (!is_array($current_elements)) {
686 $current_elements = [];
687 }
688
689 // The content is already processed by the original system (images replaced)
690 $template_content = $content;
691
692 if (!is_array($template_content)) {
693 wp_send_json_error('Invalid processed content format');
694 return;
695 }
696
697 // Generate new unique IDs for all elements to avoid conflicts
698 $template_content = $this->regenerate_element_ids($template_content);
699
700 // Merge template content with current page content
701 $current_count = count($current_elements);
702 $new_count = count($template_content);
703 $merged_elements = array_merge($current_elements, $template_content);
704 $total_count = count($merged_elements);
705
706 // error_log('King Addons Import: Merging content - current: ' . $current_count . ', new: ' . $new_count . ', total: ' . $total_count);
707
708 // Update page meta with merged content
709 $update_result = update_post_meta($page_id, '_elementor_data', wp_slash(json_encode($merged_elements)));
710 update_post_meta($page_id, '_elementor_edit_mode', 'builder');
711
712 // Force update _elementor_version to trigger cache clear
713 if (defined('ELEMENTOR_VERSION')) {
714 update_post_meta($page_id, '_elementor_version', ELEMENTOR_VERSION);
715 }
716
717 // Update page modification time to force Elementor refresh
718 wp_update_post(['ID' => $page_id, 'post_modified' => current_time('mysql'), 'post_modified_gmt' => current_time('mysql', 1)]);
719
720 // error_log('King Addons Import: Page meta updated - result: ' . ($update_result ? 'success' : 'failed'));
721
722 // Clear all Elementor caches
723 if (class_exists('\Elementor\Plugin')) {
724 \Elementor\Plugin::$instance->files_manager->clear_cache();
725 // error_log('King Addons Import: Elementor cache cleared');
726 }
727
728 // Clean up transients
729 delete_transient('elementor_import_content');
730 delete_transient('elementor_import_images');
731 delete_transient('elementor_import_total_images');
732 delete_transient('elementor_import_images_processed');
733 delete_transient('elementor_import_image_retry_count');
734 delete_transient('elementor_import_page_title');
735 delete_transient('elementor_import_elementor_version');
736 delete_transient('elementor_import_existing_page_id');
737 delete_transient('elementor_import_create_new_page');
738
739 // error_log('King Addons Import: Merge completed successfully');
740
741 wp_send_json_success([
742 'message' => 'Template merged successfully',
743 'imported_elements' => $new_count,
744 'page_id' => $page_id,
745 'current_elements_before' => $current_count,
746 'total_elements_after' => $total_count
747 ]);
748 }
749
750 /**
751 * AJAX handler for getting sections catalog data for popup
752 */
753 public function get_sections_catalog(): void
754 {
755 if (!current_user_can('edit_posts')) {
756 wp_send_json_error('Insufficient permissions');
757 return;
758 }
759
760 if (!wp_verify_nonce($_POST['nonce'], 'king_addons_template_catalog')) {
761 wp_send_json_error('Invalid nonce');
762 return;
763 }
764
765 if (!class_exists('King_Addons\\SectionsMap')) {
766 require_once KING_ADDONS_PATH . 'includes/SectionsMap.php';
767 }
768
769 $sections_map = SectionsMap::getSectionsMapArray();
770 $sections = $sections_map['sections'] ?? [];
771
772 $is_premium_active = function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code();
773
774 // Get filters from request
775 $search_query = sanitize_text_field($_POST['search'] ?? '');
776 $selected_category = sanitize_text_field($_POST['category'] ?? '');
777 $selected_type = sanitize_text_field($_POST['section_type'] ?? '');
778 $selected_plan = sanitize_text_field($_POST['plan'] ?? '');
779 $current_page = max(1, intval($_POST['page'] ?? 1));
780
781 // Get categories and section types for filters
782 $categories = [];
783 $section_types = [];
784
785 foreach ($sections as $section_key => $section) {
786 if (!in_array($section['category'], $categories)) {
787 $categories[] = $section['category'];
788 }
789 if (!in_array($section['section_type'], $section_types)) {
790 $section_types[] = $section['section_type'];
791 }
792 }
793
794 sort($categories);
795 sort($section_types);
796
797 // Filter sections
798 $filtered_sections = [];
799
800 foreach ($sections as $section_key => $section) {
801 // Add section key for frontend
802 $section['section_key'] = $section_key;
803
804 // Skip premium sections if user doesn't have premium license
805 if ($section['plan'] === 'premium' && !$is_premium_active) {
806 continue;
807 }
808
809 // Apply search filter
810 if (!empty($search_query)) {
811 $found_in_title = stripos($section['title'], $search_query) !== false;
812 $found_in_tags = false;
813
814 foreach ($section['tags'] ?? [] as $tag) {
815 if (stripos($tag, $search_query) !== false) {
816 $found_in_tags = true;
817 break;
818 }
819 }
820
821 if (!$found_in_title && !$found_in_tags) {
822 continue;
823 }
824 }
825
826 // Apply category filter
827 if (!empty($selected_category) && $section['category'] !== $selected_category) {
828 continue;
829 }
830
831 // Apply section type filter
832 if (!empty($selected_type) && $section['section_type'] !== $selected_type) {
833 continue;
834 }
835
836 // Apply plan filter
837 if (!empty($selected_plan) && $section['plan'] !== $selected_plan) {
838 continue;
839 }
840
841 $filtered_sections[] = $section;
842 }
843
844 // Pagination
845 $items_per_page = 20;
846 $total_sections = count($filtered_sections);
847 $total_pages = ceil($total_sections / $items_per_page);
848 $offset = ($current_page - 1) * $items_per_page;
849 $paged_sections = array_slice($filtered_sections, $offset, $items_per_page);
850
851 wp_send_json_success([
852 'sections' => $paged_sections,
853 'categories' => $categories,
854 'section_types' => $section_types,
855 'pagination' => [
856 'current_page' => $current_page,
857 'total_pages' => $total_pages,
858 'total_sections' => $total_sections,
859 'items_per_page' => $items_per_page
860 ],
861 'is_premium_active' => $is_premium_active
862 ]);
863 }
864
865 /**
866 * AJAX handler for importing section to current page
867 */
868 public function import_section_to_page(): void
869 {
870 if (!current_user_can('edit_posts')) {
871 wp_send_json_error('Insufficient permissions');
872 return;
873 }
874
875 if (!wp_verify_nonce($_POST['nonce'], 'king_addons_template_catalog')) {
876 wp_send_json_error('Invalid nonce');
877 return;
878 }
879
880 $section_key = sanitize_text_field($_POST['section_key']);
881 $section_plan = sanitize_text_field($_POST['section_plan']);
882 $is_premium_active = function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code();
883
884 // Determine API URL and install ID (same logic as templates)
885 if ($is_premium_active && $section_plan === 'premium') {
886 $api_url = 'https://api.kingaddons.com/get-section.php';
887
888 // Use the same method as original templates catalog
889 if (function_exists('king_addons_freemius')) {
890 $freemius_site = king_addons_freemius()->get_site();
891 $install_id = $freemius_site ? $freemius_site->id : 0;
892 } else {
893 $install_id = 0;
894 }
895
896 // error_log('King Addons Premium Section: Using install_id: ' . $install_id . ' for premium section: ' . $section_key);
897 } elseif ($section_plan === 'free') {
898 $api_url = 'https://api.kingaddons.com/get-section-free.php';
899 $install_id = 0;
900 // error_log('King Addons Free Section: Fetching free section: ' . $section_key);
901 } else {
902 // error_log('King Addons Section Error: Premium section requires premium license. Section: ' . $section_key . ', Plan: ' . $section_plan . ', Premium Active: ' . ($is_premium_active ? 'Yes' : 'No'));
903 wp_send_json_error('Premium section requires premium license');
904 return;
905 }
906
907 // Get section data from API (same as templates)
908 $response = wp_remote_post($api_url, [
909 'headers' => ['Content-Type' => 'application/json'],
910 'body' => json_encode([
911 'key' => $section_key,
912 'install' => $install_id,
913 ]),
914 'timeout' => 60
915 ]);
916
917 if (is_wp_error($response)) {
918 wp_send_json_error('Failed to fetch section: ' . $response->get_error_message());
919 return;
920 }
921
922 $body = wp_remote_retrieve_body($response);
923 $data = json_decode($body, true);
924
925 // error_log('King Addons Section API Response: ' . substr($body, 0, 500) . (strlen($body) > 500 ? '...' : ''));
926
927 if (!$data) {
928 // error_log('King Addons Section Error: Failed to decode JSON response');
929 wp_send_json_error('Invalid JSON response from section API');
930 return;
931 }
932
933 if (!isset($data['success']) || !$data['success']) {
934 $error_message = isset($data['message']) ? $data['message'] : 'Unknown API error';
935 // error_log('King Addons Section Error: API returned error: ' . $error_message);
936 wp_send_json_error('Section API error: ' . $error_message);
937 return;
938 }
939
940 // Return section data for frontend processing (adjust format from your API)
941 wp_send_json_success([
942 'section_data' => $data['section'], // Your API returns 'section' not 'landing'
943 'message' => 'Section data retrieved successfully'
944 ]);
945 }
946
947 /**
948 * Validate image URL for security (prevent SSRF attacks)
949 * @param string $url The URL to validate
950 * @return bool True if URL is safe, false otherwise
951 */
952 private function is_safe_image_url(string $url): bool
953 {
954 // Parse URL
955 $parsed_url = parse_url($url);
956 if (!$parsed_url || !isset($parsed_url['scheme']) || !isset($parsed_url['host'])) {
957 return false;
958 }
959
960 // Only allow HTTP/HTTPS
961 if (!in_array($parsed_url['scheme'], ['http', 'https'], true)) {
962 return false;
963 }
964
965 // Block local/private IP addresses to prevent SSRF
966 $host = $parsed_url['host'];
967
968 // Check if it's an IP address
969 if (filter_var($host, FILTER_VALIDATE_IP)) {
970 // Block private/reserved IP ranges
971 if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
972 return false;
973 }
974 }
975
976 // Block localhost and common local domains
977 $blocked_hosts = [
978 'localhost',
979 '127.0.0.1',
980 '::1',
981 'metadata.google.internal',
982 '169.254.169.254', // AWS metadata
983 ];
984
985 if (in_array(strtolower($host), $blocked_hosts, true)) {
986 return false;
987 }
988
989 // Only allow images from trusted domains (King Addons CDN)
990 $allowed_domains = [
991 'api.kingaddons.com',
992 'cdn.kingaddons.com',
993 'templates.kingaddons.com',
994 'images.kingaddons.com'
995 ];
996
997 $is_allowed_domain = false;
998 foreach ($allowed_domains as $allowed_domain) {
999 if (strtolower($host) === strtolower($allowed_domain) ||
1000 str_ends_with(strtolower($host), '.' . strtolower($allowed_domain))) {
1001 $is_allowed_domain = true;
1002 break;
1003 }
1004 }
1005
1006 return $is_allowed_domain;
1007 }
1008 }
1009