PluginProbe
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder / 51.1.83
King Addons for Elementor – 100+ Elementor Widgets, 4 000+ Elementor Templates, WooCommerce Builder, Mega Menu, Popup Builder v51.1.83
51.1.84 51.1.85 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 All 39 releases
← All changes | includes/extensions/Templates/Templates.php +1688 -0 51.1.251.1.83 View file →
@@ -1,0 +1,1688 @@
1 +<?php
2 +
3 +/** @noinspection SpellCheckingInspection, DuplicatedCode */
4 +
5 +namespace King_Addons;
6 +
7 +use Exception;
8 +use King_Addons\SectionsMap;
9 +
10 +if (!defined('ABSPATH')) {
11 + exit;
12 +}
13 +
14 +final class Templates
15 +{
16 + private static ?Templates $instance = null;
17 +
18 + public static function instance(): ?Templates
19 + {
20 + if (is_null(self::$instance)) {
21 + self::$instance = new self();
22 + }
23 + return self::$instance;
24 + }
25 +
26 + public function render_template_catalog_page(): void
27 + {
28 + $templates = TemplatesMap::getTemplatesMapArray();
29 + $collections = CollectionsMap::getCollectionsMapArray();
30 +
31 + // Load Sections for counting
32 + if (!class_exists('King_Addons\\SectionsMap')) {
33 + require_once KING_ADDONS_PATH . 'includes/SectionsMap.php';
34 + }
35 + $sections_map = SectionsMap::getSectionsMapArray();
36 + $sections = $sections_map['sections'] ?? [];
37 +
38 + uasort($collections, function ($a, $b) {
39 + return strcasecmp($a, $b);
40 + });
41 +
42 + $is_premium_active = king_addons_freemius()->can_use_premium_code();
43 +
44 + // TODO: TEST: For UI testing, it doesn't enable the real premium
45 + // $is_premium_active = false;
46 +
47 + // Arrays for categories and tags
48 + $categories = [];
49 + $tags = [];
50 + $category_counts = [];
51 + $collection_counts = [];
52 + $collection_images = [];
53 + $template_collection_map = [];
54 +
55 + // Getting unique categories and tags
56 + foreach ($templates['templates'] as $key => $template) {
57 + if (!in_array($template['category'], $categories)) {
58 + $categories[] = $template['category'];
59 + }
60 +
61 + foreach ($template['tags'] as $tag) {
62 + if (!in_array($tag, $tags)) {
63 + $tags[] = $tag;
64 + }
65 + }
66 +
67 + $category = $template['category'];
68 + $category_counts[$category] = isset($category_counts[$category]) ? $category_counts[$category] + 1 : 1;
69 +
70 + $collection_id = $template['collection'] ?? '';
71 + if ($collection_id !== '') {
72 + $collection_counts[$collection_id] = isset($collection_counts[$collection_id]) ? $collection_counts[$collection_id] + 1 : 1;
73 + if (!isset($collection_images[$collection_id])) {
74 + $collection_images[$collection_id] = $key;
75 + }
76 + $template_collection_map[$key] = $collection_id;
77 + }
78 + }
79 +
80 + // Count sections per collection
81 + $collection_sections_counts = [];
82 + foreach ($sections as $section) {
83 + $parent = $section['parent_template'] ?? '';
84 + if ($parent && isset($template_collection_map[$parent])) {
85 + $col_id = $template_collection_map[$parent];
86 + $collection_sections_counts[$col_id] = isset($collection_sections_counts[$col_id]) ? $collection_sections_counts[$col_id] + 1 : 1;
87 + }
88 + }
89 +
90 + sort($categories);
91 +
92 + // Get filters from query
93 + $search_query = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : '';
94 + $selected_category = isset($_GET['category']) ? sanitize_text_field($_GET['category']) : '';
95 + $selected_collection = isset($_GET['collection']) ? sanitize_text_field($_GET['collection']) : '';
96 + $selected_tags = isset($_GET['tags']) ? array_filter(explode(',', sanitize_text_field($_GET['tags']))) : [];
97 + $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
98 +
99 + // Use the common function to get filtered templates and pagination
100 + $result = $this->get_filtered_templates($templates, $search_query, $selected_category, $selected_tags, $selected_collection, $current_page);
101 +
102 + if (isset($_GET['ajax']) && $_GET['ajax']) {
103 + wp_send_json_success(['grid_html' => $result['grid_html'], 'pagination_html' => $result['pagination_html']]);
104 + }
105 + if ($is_premium_active) {
106 +?>
107 + <script type="text/javascript">
108 + (function() {
109 + window.kingAddons = window.kingAddons || {};
110 + window.kingAddons.installId = <?php
111 + echo json_encode(king_addons_freemius()->get_site()->id);
112 + ?>;
113 + })();
114 + </script>
115 + <?php
116 + }
117 + ?>
118 + <div id="king-addons-templates-top"></div>
119 + <div id="king-addons-templates" class="king-addons-templates">
120 + <div class="kng-intro">
121 + <div class="kng-intro-wrap">
122 + <div class="kng-intro-wrap-1">
123 + <h1 class="kng-intro-title"><?php echo esc_html__('King Addons for Elementor', 'king-addons'); ?></h1>
124 + <?php if ($is_premium_active): ?>
125 + <span class="premium-active-txt"><?php echo esc_html__('PREMIUM', 'king-addons'); ?></h1></span>
126 + <?php endif; ?>
127 + <h2 class="kng-intro-subtitle"><?php echo esc_html__('Discover professionally designed, attention-grabbing, and SEO-optimized templates perfect for any site', 'king-addons'); ?></h2>
128 + </div>
129 + <div class="kng-intro-wrap-2">
130 + <div class="kng-navigation">
131 + <div class="kng-nav-item kng-nav-item-current">
132 + <a href="<?php echo admin_url('admin.php?page=king-addons'); ?>">
133 + <div class="kng-nav-item-txt"><?php echo esc_html__('Free Widgets & Features', 'king-addons'); ?></div>
134 + </a>
135 + </div>
136 + <?php if (KING_ADDONS_EXT_HEADER_FOOTER_BUILDER): ?>
137 + <div class="kng-nav-item kng-nav-item-current">
138 + <a href="<?php echo admin_url('admin.php?page=king-addons-el-hf'); ?>">
139 + <div class="kng-nav-item-txt"><?php echo esc_html__('Free Header & Footer Builder', 'king-addons'); ?></div>
140 + </a>
141 + </div>
142 + <?php endif; ?>
143 + <?php if (KING_ADDONS_EXT_POPUP_BUILDER): ?>
144 + <div class="kng-nav-item kng-nav-item-current">
145 + <a href="<?php echo admin_url('admin.php?page=king-addons-popup-builder'); ?>">
146 + <div class="kng-nav-item-txt"><?php echo esc_html__('Free Popup Builder', 'king-addons'); ?></div>
147 + </a>
148 + </div>
149 + <?php endif; ?>
150 + <div class="kng-nav-item kng-nav-item-current">
151 + <a href="https://www.youtube.com/@kingaddons/videos" target="_blank">
152 + <div class="kng-nav-item-txt"><?php echo esc_html__('YouTube Guides', 'king-addons'); ?></div>
153 + </a>
154 + </div>
155 + <?php if (!king_addons_freemius()->can_use_premium_code()): ?>
156 + <div class="kng-nav-item kng-nav-item-current kng-nav-activate-license">
157 + <a id="activate-license-btn">
158 + <img src="<?php echo esc_url(KING_ADDONS_URL) . 'includes/admin/img/up.svg'; ?>"
159 + alt="<?php echo esc_html__('Activate License', 'king-addons'); ?>">
160 + <div class="kng-nav-item-txt"><?php echo esc_html__('Activate License', 'king-addons'); ?></div>
161 + </a>
162 + </div>
163 + <?php endif; ?>
164 + <?php if (!king_addons_freemius()->can_use_premium_code()): ?>
165 + <div class="kng-promo-btn-wrap">
166 + <a href="https://kingaddons.com/pricing/?utm_source=king-addons-templates-catalog" target="_blank">
167 + <div class="kng-promo-btn-txt">
168 + <?php esc_html_e('Unlock Premium Features & 650+ Templates Today!', 'king-addons'); ?>
169 + </div>
170 + <img width="16px"
171 + src="<?php echo esc_url(KING_ADDONS_URL) . 'includes/admin/img/share-v2.svg'; ?>"
172 + alt="<?php echo esc_html__('Open link in the new tab', 'king-addons'); ?>">
173 + </a>
174 + </div>
175 + <?php endif; ?>
176 + </div>
177 + </div>
178 + </div>
179 + </div>
180 + <!-- Catalog Tabs -->
181 + <div class="king-addons-catalog-tabs">
182 + <button class="king-addons-tab-button active" data-tab="templates">
183 + <i class="eicon-document-file"></i>
184 + <?php esc_html_e('Templates', 'king-addons'); ?>
185 + <span class="tab-count"><?php echo count($templates['templates']); ?></span>
186 + </button>
187 + <button class="king-addons-tab-button" data-tab="sections">
188 + <i class="eicon-section"></i>
189 + <?php esc_html_e('Sections', 'king-addons'); ?>
190 + <span class="tab-count" id="sections-count">0</span>
191 + </button>
192 + <button class="king-addons-tab-button" data-tab="collections">
193 + <i class="eicon-folder"></i>
194 + <?php esc_html_e('Collections', 'king-addons'); ?>
195 + <span class="tab-count"><?php echo count($collections); ?></span>
196 + </button>
197 + </div>
198 +
199 + <!-- Templates Tab Content -->
200 + <div id="templates-catalog" class="king-addons-tab-content active">
201 + <div class="filters-wrapper">
202 + <div class="filters">
203 + <select id="template-category">
204 + <option value=""><?php esc_html_e('All Categories', 'king-addons'); ?>
205 + (<?php echo count($templates['templates']); ?>)
206 + </option>
207 + <?php foreach ($categories as $category): ?>
208 + <option value="<?php echo esc_attr($category); ?>" <?php selected($selected_category, $category); ?>>
209 + <?php echo esc_html(ucwords(str_replace('-', ' ', $category))); ?>
210 + (<?php echo $category_counts[$category]; ?>)
211 + </option>
212 + <?php endforeach; ?>
213 + </select>
214 + <select id="template-collection">
215 + <option value=""><?php esc_html_e('All Collections', 'king-addons'); ?>
216 + (<?php echo count($collections); ?>)
217 + </option>
218 + <?php foreach ($collections as $id => $name): ?>
219 + <option value="<?php echo esc_attr($id); ?>" <?php selected($selected_collection, $id); ?>>
220 + <?php echo esc_html($name); ?>
221 + </option>
222 + <?php endforeach; ?>
223 + </select>
224 + <div id="template-tags">
225 + <?php
226 + shuffle($tags); ?>
227 + <div class="tags-header">Tags</div>
228 + <?php foreach ($tags as $tag): ?>
229 + <label>
230 + <input type="checkbox"
231 + value="<?php echo esc_attr($tag); ?>" <?php echo in_array($tag, $selected_tags) ? 'checked' : ''; ?>> <?php echo esc_html(ucwords(str_replace('-', ' ', $tag))); ?>
232 + </label>
233 + <?php endforeach; ?>
234 + </div>
235 + <button id="reset-filters"><?php esc_html_e('Reset Search & Filters', 'king-addons'); ?></button>
236 + <?php if (!$is_premium_active): ?>
237 + <div class="promo-wrapper">
238 + <div class="promo-txt"><?php
239 + esc_html_e('Unlock Premium Templates', 'king-addons');
240 + echo '<ul><li>$6.99/month</li>';
241 + echo '<li>Unlimited Downloads</li>';
242 + echo '<li>Keep all templates forever</li></ul>';
243 + ?></div>
244 + <a class="purchase-btn"
245 + href="https://kingaddons.com/pricing/?utm_source=kng-templates-banner-side&utm_medium=plugin&utm_campaign=kng"
246 + target="_blank">
247 + <button class="promo-btn purchase-btn"
248 + style="display: flex;align-items: center;">
249 + <img src="<?php echo esc_url(KING_ADDONS_URL) . 'includes/admin/img/icon-for-admin.svg'; ?>"
250 + style="margin-right: 5px;width: 14px;height: 14px;"
251 + alt="<?php echo esc_html__('Upgrade Now', 'king-addons'); ?>"><?php esc_html_e('Upgrade Now', 'king-addons'); ?>
252 + </button>
253 + </a>
254 + </div>
255 + <?php endif; ?>
256 + </div>
257 + </div>
258 + <div class="templates-grid-wrapper">
259 + <div class="search-wrapper">
260 + <input type="text" id="template-search" value="<?php echo esc_attr($search_query); ?>"
261 + placeholder="<?php esc_attr_e('Search templates...', 'king-addons'); ?>">
262 + </div>
263 + <div class="templates-grid">
264 + <?php echo $result['grid_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
265 + ?>
266 + </div>
267 + <div class="pagination">
268 + <?php echo $result['pagination_html']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
269 + ?>
270 + </div>
271 + </div>
272 + </div>
273 +
274 + <!-- Sections Tab Content -->
275 + <div id="sections-catalog" class="king-addons-tab-content">
276 + <div class="filters-wrapper">
277 + <div class="filters">
278 + <select id="sections-category">
279 + <option value=""><?php esc_html_e('All Categories', 'king-addons'); ?></option>
280 + </select>
281 + <select id="sections-type">
282 + <option value=""><?php esc_html_e('All Types', 'king-addons'); ?></option>
283 + </select>
284 + <select id="sections-plan">
285 + <option value=""><?php esc_html_e('All Plans', 'king-addons'); ?></option>
286 + <option value="free"><?php esc_html_e('Free', 'king-addons'); ?></option>
287 + <?php if ($is_premium_active): ?>
288 + <option value="premium"><?php esc_html_e('Premium', 'king-addons'); ?></option>
289 + <?php endif; ?>
290 + </select>
291 + <button id="sections-reset-filters"><?php esc_html_e('Reset Search & Filters', 'king-addons'); ?></button>
292 + </div>
293 + </div>
294 + <div class="sections-grid-wrapper">
295 + <div class="search-wrapper">
296 + <input type="text" id="sections-search" placeholder="<?php esc_attr_e('Search sections...', 'king-addons'); ?>">
297 + </div>
298 + <div class="sections-grid">
299 + <div class="sections-loading">
300 + <?php esc_html_e('Loading sections...', 'king-addons'); ?>
301 + </div>
302 + </div>
303 + <div class="sections-pagination"></div>
304 + </div>
305 + </div>
306 +
307 + <!-- Collections Tab Content -->
308 + <div id="collections-catalog" class="king-addons-tab-content">
309 + <div class="collections-toolbar">
310 + <div class="collections-search">
311 + <input type="text" id="collections-search" placeholder="<?php esc_attr_e('Search collections...', 'king-addons'); ?>">
312 + </div>
313 + </div>
314 +
315 + <div class="collections-rows-wrapper">
316 + <div class="collections-rows-container">
317 + <div class="collections-loading">
318 + <?php esc_html_e('Loading collections...', 'king-addons'); ?>
319 + </div>
320 + </div>
321 + <div class="collections-rows-pagination"></div>
322 + </div>
323 +
324 + <!-- Collection Details View -->
325 + <div id="collection-details-view" style="display: none;">
326 + <div class="collection-details-header">
327 + <button type="button" class="ka-back-button">
328 + <i class="eicon-arrow-left"></i> <?php esc_html_e('Back to Collections', 'king-addons'); ?>
329 + </button>
330 + <div class="collection-details-search">
331 + <input type="text" id="collection-search" placeholder="<?php esc_attr_e('Search in collection...', 'king-addons'); ?>">
332 + </div>
333 + </div>
334 + <div class="collection-details-content">
335 + <div class="collection-loading">
336 + <?php esc_html_e('Loading collection...', 'king-addons'); ?>
337 + </div>
338 + </div>
339 + </div>
340 + </div>
341 +
342 + <div id="template-preview-popup" style="display:none;">
343 + <div class="popup-content">
344 + <div class="popup-content-nav">
345 + <button data-plan-active="<?php echo ($is_premium_active) ? 'premium' : 'free'; ?>"
346 + id="install-template">
347 + <?php esc_html_e('Import Template', 'king-addons'); ?>
348 + </button>
349 + <a href="#" id="template-preview-link" target="_blank">
350 + <?php esc_html_e('Live Preview', 'king-addons'); ?>
351 + </a>
352 + <div class="preview-mode-switcher">
353 + <button data-mode="desktop" class="active" id="preview-desktop">
354 + <span class="dashicons dashicons-desktop"></span>
355 + </button>
356 + <button data-mode="tablet" id="preview-tablet">
357 + <span class="dashicons dashicons-tablet"></span>
358 + </button>
359 + <button data-mode="mobile" id="preview-mobile">
360 + <span class="dashicons dashicons-smartphone"></span>
361 + </button>
362 + </div>
363 + <button id="close-popup">
364 + <?php esc_html_e('Close Preview X', 'king-addons'); ?>
365 + </button>
366 + </div>
367 + <iframe id="template-preview-iframe" src="" frameborder="0"></iframe>
368 + </div>
369 + </div>
370 +
371 + <!-- Section Import Popup -->
372 + <div id="section-import-popup" class="ka-popup-overlay" style="display:none;">
373 + <div class="ka-popup-content">
374 + <button type="button" class="ka-popup-close" id="close-section-popup">
375 + <span class="dashicons dashicons-no-alt"></span>
376 + </button>
377 + <div class="ka-popup-body">
378 + <div class="ka-popup-icon">
379 + <i class="eicon-file-download"></i>
380 + </div>
381 + <h3 class="ka-popup-title"><?php esc_html_e('Import Section', 'king-addons'); ?></h3>
382 + <p class="ka-popup-message">
383 + <?php esc_html_e('Are you sure you want to import this section? It will be added to your current page.', 'king-addons'); ?>
384 + </p>
385 + <div class="ka-popup-details">
386 + <div class="ka-popup-detail-row">
387 + <span class="ka-label"><?php esc_html_e('Section:', 'king-addons'); ?></span>
388 + <span class="ka-value" id="popup-section-name"></span>
389 + </div>
390 + <div class="ka-popup-detail-row">
391 + <span class="ka-label"><?php esc_html_e('Plan:', 'king-addons'); ?></span>
392 + <span class="ka-value" id="popup-section-plan"></span>
393 + </div>
394 + </div>
395 + <div class="ka-popup-actions">
396 + <button type="button" class="ka-btn ka-btn-secondary" id="cancel-section-import">
397 + <?php esc_html_e('Cancel', 'king-addons'); ?>
398 + </button>
399 + <button type="button" class="ka-btn ka-btn-primary" id="confirm-section-import">
400 + <?php esc_html_e('Import Section', 'king-addons'); ?>
401 + </button>
402 + </div>
403 + </div>
404 + </div>
405 + </div>
406 + <div id="template-installing-popup" style="display:none;">
407 + <div class="popup-content">
408 + <div id="progress-container">
409 + <div id="progress-bar">
410 + 0%
411 + </div>
412 + </div>
413 + <div id="progress"></div>
414 + <div id="image-list"></div>
415 + <div id="final_response"></div>
416 + <button id="close-installing-popup"
417 + style="display:none;"><?php esc_html_e('Close', 'king-addons'); ?></button>
418 + <a href="#" id="go-to-imported-page"
419 + style="display:none;"><?php esc_html_e('Go to imported page', 'king-addons'); ?></a>
420 + </div>
421 + </div>
422 + <div id="license-activating-popup" style="display:none;">
423 + <div class="license-activating-popup-content">
424 + <div class="license-activating-popup-txt"><?php esc_html_e('1. Download and install the premium version of the plugin - King Addons Pro. You can find the link in the email received after the license purchase.', 'king-addons'); ?></div>
425 + <div class="license-activating-popup-txt"><?php esc_html_e('2. Go to the Plugins page.', 'king-addons'); ?></div>
426 + <div class="license-activating-popup-txt"><?php esc_html_e('3. Find the King Addons Pro plugin.', 'king-addons'); ?></div>
427 + <div class="license-activating-popup-txt"><?php esc_html_e('4. Click on Activate License link.', 'king-addons'); ?></div>
428 + <div class="license-activating-popup-txt"><?php esc_html_e('5. Enter the License Key provided in the email. Done!', 'king-addons'); ?></div>
429 + <button id="close-license-activating-popup"><?php esc_html_e('Close', 'king-addons'); ?></button>
430 + </div>
431 + </div>
432 + <div id="premium-promo-popup" style="display:none;">
433 + <div class="premium-promo-popup-content">
434 + <div class="premium-promo-popup-wrapper">
435 + <div class="premium-promo-popup-txt"><?php
436 +
437 + echo '<span class="pr-popup-title">Want This Premium Template?</span>';
438 + echo '<br><span class="pr-popup-desc">';
439 + echo 'Get <span class="pr-popup-desc-b">unlimited downloads</span> for just';
440 + echo ' <span class="pr-popup-desc-b">$6.99/month';
441 + echo '</span> — keep them <span class="pr-popup-desc-b">even after</span> your subscription ends!';
442 + echo '</span><span class="pr-popup-desc" style="font-size: 16px;opacity: 0.6;">Trusted by 20,000+ users</span>';
443 + ?></div>
444 + <a class="purchase-btn"
445 + href="https://kingaddons.com/pricing/?utm_source=kng-templates-banner-pro&utm_medium=plugin&utm_campaign=kng"
446 + target="_blank">
447 + <button class="premium-promo-popup-purchase-btn purchase-btn">
448 + <img src="<?php echo esc_url(KING_ADDONS_URL) . 'includes/admin/img/icon-for-admin.svg'; ?>"
449 + style="margin-right: 7px;width: 16px;height: 16px;"
450 + alt="<?php echo esc_html__('Unlock All Templates', 'king-addons'); ?>"><?php esc_html_e('Unlock All Templates', 'king-addons'); ?>
451 + </button>
452 + </a>
453 + <button id="close-premium-promo-popup"><?php esc_html_e('Close', 'king-addons'); ?></button>
454 + </div>
455 + </div>
456 + </div>
457 + </div>
458 + <?php
459 + }
460 +
461 + private function get_filtered_templates($templates, $search_query, $selected_category, $selected_tags, $selected_collection, $current_page): array
462 + {
463 + $search_terms = array_filter(explode(' ', $search_query));
464 + $has_search = !empty($search_terms);
465 + // Custom: direct key search
466 + $search_by_key = false;
467 + $found_by_key = [];
468 + if (!empty($search_query) && isset($templates['templates'][$search_query])) {
469 + $found_by_key[] = $templates['templates'][$search_query];
470 + $found_by_key[0]['template_key'] = $search_query;
471 + $search_by_key = true;
472 + }
473 +
474 + // Filter templates based on search and selected filters
475 + if ($search_by_key) {
476 + $filtered_templates = $found_by_key;
477 + } elseif (!$has_search) {
478 + $filtered_templates = array_filter($templates['templates'], function ($template) use ($search_terms, $selected_category, $selected_tags, $selected_collection) {
479 +
480 + foreach ($search_terms as $term) {
481 + $found_in_title = stripos($template['title'], $term) !== false;
482 + $found_in_tags = false;
483 +
484 + foreach ($template['tags'] as $tag) {
485 + if (stripos($tag, $term) !== false) {
486 + $found_in_tags = true;
487 + break;
488 + }
489 + }
490 +
491 + if (!$found_in_title && !$found_in_tags) {
492 + return false;
493 + }
494 + }
495 +
496 + if ($selected_category && $template['category'] !== $selected_category) {
497 + return false;
498 + }
499 +
500 + if ($selected_tags) {
501 + $template_tags = $template['tags'];
502 + foreach ($selected_tags as $tag) {
503 + if (!in_array($tag, $template_tags)) {
504 + return false;
505 + }
506 + }
507 + }
508 +
509 + if ($selected_collection && $template['collection'] != $selected_collection) {
510 + return false;
511 + }
512 +
513 + return true;
514 + });
515 +
516 + // Shuffle templates
517 + // shuffle($filtered_templates);
518 +
519 + } else {
520 +
521 + $filtered_templates = $templates['templates'];
522 +
523 + if (!empty($search_terms) || !empty($selected_category) || !empty($selected_tags) || !empty($selected_collection)) {
524 + $matched_by_title = [];
525 + $matched_by_tags = [];
526 +
527 + foreach ($filtered_templates as $key => $template) {
528 +
529 + $found_in_title = false;
530 + $found_in_tags = false;
531 +
532 + foreach ($search_terms as $term) {
533 + if ($term && stripos($template['title'], $term) !== false) {
534 + $found_in_title = true;
535 + }
536 +
537 + foreach ($template['tags'] as $tag) {
538 + if (stripos($tag, $term) !== false) {
539 + $found_in_tags = true;
540 + break;
541 + }
542 + }
543 + // if (!$found_in_title && !$found_in_tags) {
544 + // continue;
545 + // }
546 + }
547 +
548 + if ($selected_category && $template['category'] !== $selected_category) {
549 + continue;
550 + }
551 +
552 + if ($selected_tags) {
553 + $template_tags = $template['tags'];
554 + foreach ($selected_tags as $tag) {
555 + if (!in_array($tag, $template_tags)) {
556 + continue 2;
557 + }
558 + }
559 + }
560 +
561 + if ($selected_collection && $template['collection'] != $selected_collection) {
562 + continue;
563 + }
564 +
565 + // Attach the template key for later use
566 + $template['template_key'] = $key;
567 +
568 + if ($found_in_title) {
569 + $matched_by_title[] = $template;
570 + } elseif ($found_in_tags) {
571 + $matched_by_tags[] = $template;
572 + }
573 + }
574 +
575 + // Combine the arrays, so templates matching by title come first
576 + $filtered_templates = array_merge($matched_by_title, $matched_by_tags);
577 + }
578 + }
579 +
580 + // Pagination setup
581 + $items_per_page = 20;
582 + $total_templates = count($filtered_templates);
583 + $offset = ($current_page - 1) * $items_per_page;
584 + $paged_templates = array_slice($filtered_templates, $offset, $items_per_page);
585 +
586 + ob_start();
587 + if (empty($paged_templates)) {
588 + echo '<p class="templates-not-found">' . esc_html__('No templates found.', 'king-addons') . '</p>';
589 + } else {
590 + foreach ($paged_templates as $key => $template) {
591 + $attr_key = ($has_search) ? $template['template_key'] : $key;
592 + ?>
593 + <div class="template-item"
594 + data-category="<?php echo esc_attr($template['category']); ?>"
595 + data-tags="<?php echo esc_attr(implode(',', $template['tags'])); ?>"
596 + data-template-key="<?php echo esc_attr($attr_key); ?>"
597 + data-template-plan="<?php echo esc_attr($template['plan']); ?>">
598 + <img class="kng-addons-template-thumbnail" loading="lazy"
599 + src="<?php echo esc_url("https://thumbnails.kingaddons.com/$attr_key.png?v=4"); ?>"
600 + alt="<?php echo esc_attr($template['title']); ?>">
601 + <h3><?php echo esc_html($template['title']); ?></h3>
602 + <div class="template-plan template-plan-<?php echo esc_html($template['plan']); ?>"><?php echo esc_html($template['plan']); ?></div>
603 + </div>
604 +<?php
605 + }
606 + }
607 + $grid_html = ob_get_clean();
608 +
609 + ob_start();
610 +
611 + $pages = paginate_links(array(
612 + 'base' => add_query_arg(array(
613 + 'paged' => '%#%',
614 + 's' => $search_query,
615 + 'category' => $selected_category,
616 + 'collection' => $selected_collection,
617 + 'tags' => implode(',', $selected_tags),
618 + )),
619 + 'format' => '?paged=%#%',
620 + 'current' => $current_page,
621 + 'total' => ceil($total_templates / $items_per_page),
622 + 'prev_text' => __('&larr; Previous', 'king-addons'),
623 + 'next_text' => __('Next &rarr;', 'king-addons'),
624 + 'end_size' => 9,
625 + 'mid_size' => 3,
626 + ));
627 + if ($pages) {
628 + echo '<div id="king-addons-pagination-inner-wrap" class="pagination-inner-wrap"><div class="pagination-inner">';
629 + echo $pages; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
630 + echo '</div></div>';
631 + }
632 +
633 + $pagination_html = ob_get_clean();
634 +
635 + return ['grid_html' => $grid_html, 'pagination_html' => $pagination_html];
636 + }
637 +
638 + public function king_addons_enqueue_scripts(): void
639 + {
640 + $screen = get_current_screen();
641 + if ($screen->id === 'toplevel_page_king-addons-templates') {
642 + wp_enqueue_style('king-addons-templates-style', KING_ADDONS_URL . 'includes/admin/css/templates.css', '', KING_ADDONS_VERSION);
643 +
644 + if (!wp_script_is(KING_ADDONS_ASSETS_UNIQUE_KEY . '-' . 'jquery' . '-' . 'jquery')) {
645 + wp_enqueue_script(KING_ADDONS_ASSETS_UNIQUE_KEY . '-' . 'jquery' . '-' . 'jquery', '', '', KING_ADDONS_VERSION);
646 + }
647 +
648 + wp_enqueue_script('king-addons-templates-script', KING_ADDONS_URL . 'includes/admin/js/templates.js', '', KING_ADDONS_VERSION, true);
649 +
650 + wp_localize_script('king-addons-templates-script', 'kingAddonsData', array(
651 + 'adminUrl' => admin_url('admin-post.php'),
652 + 'ajaxUrl' => admin_url('admin-ajax.php'),
653 + 'nonce' => wp_create_nonce('kingAddonsNonce'),
654 + ));
655 + }
656 + // if ($screen->id === 'king-addons_page_king-addons-account') {
657 + // wp_enqueue_style('king-addons-account-style', KING_ADDONS_URL . 'includes/admin/css/account.css', '', KING_ADDONS_VERSION);
658 + // }
659 + }
660 +
661 + public function __construct()
662 + {
663 + add_action('admin_enqueue_scripts', array($this, 'king_addons_enqueue_scripts'));
664 +
665 + // Get the performance setting
666 + $improve_import = get_option('king_addons_improve_import_performance', '1');
667 +
668 + // Conditionally add time limit adjustments based on setting
669 + if ($improve_import === '1') {
670 + add_action('wp_ajax_import_elementor_page_with_images', function () {
671 + @set_time_limit(300);
672 + }, 0);
673 +
674 + add_action('wp_ajax_process_import_images', function () {
675 + @set_time_limit(300);
676 + }, 0);
677 + }
678 +
679 + add_action(
680 + 'wp_ajax_import_elementor_page_with_images',
681 + [$this, 'import_elementor_page_with_images']
682 + );
683 +
684 + add_action(
685 + 'wp_ajax_process_import_images',
686 + [$this, 'process_import_images']
687 + );
688 +
689 + add_action('wp_ajax_filter_templates', array($this, 'handle_filter_templates'));
690 + add_action('wp_ajax_king_addons_get_collection_details', array($this, 'handle_get_collection_details'));
691 + add_action('wp_ajax_king_addons_get_collections_rows', array($this, 'handle_get_collections_rows'));
692 +
693 + // Sections catalog endpoints
694 + add_action('wp_ajax_king_addons_get_sections_catalog', array($this, 'handle_get_sections_catalog'));
695 + add_action('wp_ajax_king_addons_import_section_admin', array($this, 'handle_import_section_to_page'));
696 +
697 + add_action('http_api_curl', array($this, 'set_custom_curl_options'), 10, 3);
698 + }
699 +
700 + public function set_custom_curl_options($handle, $r, $url)
701 + {
702 + if (strpos($url, 'kingaddons.com') !== false) {
703 + curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 60);
704 + curl_setopt($handle, CURLOPT_DNS_CACHE_TIMEOUT, 300);
705 + curl_setopt($handle, CURLOPT_TIMEOUT, 300);
706 + }
707 + }
708 +
709 + function handle_filter_templates(): void
710 + {
711 + if (!current_user_can('manage_options')) {
712 + return;
713 + }
714 +
715 + if (!isset($_POST['action']) || $_POST['action'] !== 'filter_templates') {
716 + wp_send_json_error('Invalid request');
717 + return;
718 + }
719 +
720 + $search_query = isset($_POST['s']) ? sanitize_text_field($_POST['s']) : '';
721 + $selected_category = isset($_POST['category']) ? sanitize_text_field($_POST['category']) : '';
722 + $selected_collection = isset($_POST['collection']) ? sanitize_text_field($_POST['collection']) : '';
723 + $selected_tags = isset($_POST['tags']) ? array_filter(explode(',', sanitize_text_field($_POST['tags']))) : [];
724 + $current_page = isset($_POST['paged']) ? max(1, intval($_POST['paged'])) : 1;
725 +
726 + $templates = TemplatesMap::getTemplatesMapArray();
727 +
728 + // Use the common function to get filtered templates and pagination
729 + $result = $this->get_filtered_templates($templates, $search_query, $selected_category, $selected_tags, $selected_collection, $current_page);
730 +
731 + wp_send_json_success(['grid_html' => $result['grid_html'], 'pagination_html' => $result['pagination_html']]);
732 + }
733 +
734 + public function import_elementor_page_with_images(): void
735 + {
736 + if (!current_user_can('manage_options')) {
737 + wp_send_json_error('Insufficient permissions');
738 + return;
739 + }
740 +
741 + // Support both the legacy templates admin nonce and the Elementor editor
742 + // Template Catalog button nonce.
743 + $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
744 + $valid_nonce = $nonce && (
745 + wp_verify_nonce($nonce, 'kingAddonsNonce') ||
746 + wp_verify_nonce($nonce, 'king_addons_template_catalog')
747 + );
748 +
749 + if (!$valid_nonce) {
750 + wp_send_json_error('Invalid nonce');
751 + return;
752 + }
753 +
754 + $import_data = json_decode(stripslashes($_POST['data']), true);
755 +
756 + if (json_last_error() !== JSON_ERROR_NONE) {
757 + wp_send_json_error('JSON decode error: ' . json_last_error_msg());
758 + return;
759 + }
760 +
761 + if (isset($import_data['content']) && isset($import_data['images']) && isset($import_data['title'])) {
762 + $content = $import_data['content'];
763 + $image_data = $import_data['images'];
764 +
765 + $page_title = sanitize_text_field($import_data['title']);
766 + $elementor_version = sanitize_text_field($import_data['elementor_version']);
767 +
768 + // Check if this is for existing page (from popup import)
769 + $existing_page_id = isset($import_data['existing_page_id']) ? intval($import_data['existing_page_id']) : 0;
770 + $create_new_page = isset($import_data['create_new_page']) ? (bool)$import_data['create_new_page'] : true;
771 +
772 + if ($existing_page_id && !current_user_can('edit_post', $existing_page_id)) {
773 + wp_send_json_error('Insufficient permissions');
774 + return;
775 + }
776 +
777 + delete_transient('elementor_import_content');
778 + delete_transient('elementor_import_images');
779 + delete_transient('elementor_import_total_images');
780 + delete_transient('elementor_import_images_processed');
781 + delete_transient('elementor_import_image_retry_count');
782 + delete_transient('elementor_import_page_title');
783 + delete_transient('elementor_import_elementor_version');
784 + delete_transient('elementor_import_existing_page_id');
785 + delete_transient('elementor_import_create_new_page');
786 +
787 + set_transient('elementor_import_content', $content, 60 * 60);
788 + set_transient('elementor_import_images', $image_data, 60 * 60);
789 + set_transient('elementor_import_total_images', count($image_data), 60 * 60);
790 + set_transient('elementor_import_images_processed', 0, 60 * 60);
791 + set_transient('elementor_import_image_retry_count', [], 60 * 60);
792 + set_transient('elementor_import_page_title', $page_title, 60 * 60);
793 + set_transient('elementor_import_elementor_version', $elementor_version, 60 * 60);
794 + set_transient('elementor_import_existing_page_id', $existing_page_id, 60 * 60);
795 + set_transient('elementor_import_create_new_page', $create_new_page, 60 * 60);
796 +
797 + // error_log('King Addons Import: Initialized ' . ($create_new_page ? 'new page' : 'existing page') . ' import. Existing page ID: ' . $existing_page_id);
798 +
799 + wp_send_json_success([
800 + 'message' => 'Import initialized.',
801 + 'images' => $image_data,
802 + 'existing_page_id' => $existing_page_id,
803 + 'create_new_page' => $create_new_page
804 + ]);
805 + } else {
806 + wp_send_json_error('Invalid import data.');
807 + }
808 + }
809 +
810 + private function replace_image_data(array &$array, $old_url, $new_url, $old_id, $new_id)
811 + {
812 + foreach ($array as $key => &$value) {
813 + if (is_array($value)) {
814 + // Standard structure with both 'url' and 'id'
815 + if (
816 + isset($value['url'], $value['id']) &&
817 + $value['url'] === $old_url &&
818 + $value['id'] === $old_id
819 + ) {
820 + $value['url'] = $new_url;
821 + $value['id'] = $new_id;
822 + }
823 +
824 + // Background image structure (common in Elementor)
825 + if (
826 + isset($value['url']) &&
827 + $value['url'] === $old_url &&
828 + (!isset($value['id']) || $value['id'] === $old_id)
829 + ) {
830 + $value['url'] = $new_url;
831 + if (isset($value['id'])) {
832 + $value['id'] = $new_id;
833 + }
834 + }
835 +
836 + // Recursively check deeper nested arrays
837 + $this->replace_image_data($value, $old_url, $new_url, $old_id, $new_id);
838 + } elseif (is_string($value)) {
839 + // Direct URL string replacement
840 + if ($value === $old_url) {
841 + $array[$key] = $new_url;
842 + }
843 +
844 + // CSS background-image style replacement
845 + if (strpos($value, 'background-image:') !== false && strpos($value, $old_url) !== false) {
846 + $array[$key] = str_replace($old_url, $new_url, $value);
847 + }
848 +
849 + // URL() function replacement in CSS
850 + if (strpos($value, 'url(') !== false && strpos($value, $old_url) !== false) {
851 + $array[$key] = str_replace($old_url, $new_url, $value);
852 + }
853 +
854 + // JSON string containing URLs (for widget settings)
855 + if (strpos($value, '{') === 0 && strpos($value, $old_url) !== false) {
856 + $decoded = json_decode($value, true);
857 + if (is_array($decoded)) {
858 + $this->replace_image_data($decoded, $old_url, $new_url, $old_id, $new_id);
859 + $array[$key] = json_encode($decoded);
860 + }
861 + }
862 + }
863 + }
864 + }
865 +
866 + public function process_import_images(): void
867 + {
868 + // Conditionally increase execution time limit based on setting
869 + $improve_import = get_option('king_addons_improve_import_performance', '1');
870 + if ($improve_import === '1') {
871 + @set_time_limit(300);
872 + }
873 +
874 + if (!current_user_can('manage_options')) {
875 + wp_send_json_error('The current user can not manage options and create pages. Please change it in the WordPress settings.');
876 + return;
877 + }
878 +
879 + // Support both the legacy templates admin nonce and the Elementor editor
880 + // Template Catalog button nonce.
881 + $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
882 + $valid_nonce = $nonce && (
883 + wp_verify_nonce($nonce, 'kingAddonsNonce') ||
884 + wp_verify_nonce($nonce, 'king_addons_template_catalog')
885 + );
886 +
887 + if (!$valid_nonce) {
888 + wp_send_json_error('Invalid nonce');
889 + return;
890 + }
891 +
892 + $start_time = time();
893 + $timeout = 30;
894 +
895 + $content = get_transient('elementor_import_content');
896 + $image_data = get_transient('elementor_import_images');
897 + $total_images = get_transient('elementor_import_total_images');
898 + $images_processed = get_transient('elementor_import_images_processed');
899 + $image_retry_count = get_transient('elementor_import_image_retry_count');
900 + $page_title = get_transient('elementor_import_page_title');
901 + $elementor_version = get_transient('elementor_import_elementor_version');
902 + $existing_page_id = get_transient('elementor_import_existing_page_id');
903 + $create_new_page = get_transient('elementor_import_create_new_page');
904 +
905 + if (!is_array($image_retry_count)) {
906 + $image_retry_count = [];
907 + }
908 +
909 + if ($images_processed < $total_images) {
910 + $current_image = $image_data[$images_processed];
911 + $url = $current_image['url'];
912 + if (!isset($image_retry_count[$url])) {
913 + $image_retry_count[$url] = 0;
914 + }
915 +
916 + $new_image_id = $this->download_image_to_media_gallery($url, $image_retry_count[$url]);
917 + if ($new_image_id === false) {
918 + $image_retry_count[$url]++;
919 + set_transient('elementor_import_image_retry_count', $image_retry_count, 60 * 60);
920 +
921 + if ($image_retry_count[$url] > 3) {
922 + $images_processed++;
923 + set_transient('elementor_import_images_processed', $images_processed, 60 * 60);
924 + $progress = round(($images_processed / $total_images) * 90) + 10;
925 +
926 + wp_send_json_success([
927 + 'progress' => $progress,
928 + 'message' => "Processed $images_processed of $total_images images.",
929 + 'image_url' => $url,
930 + 'images_processed' => $images_processed,
931 + 'new_image_id' => 'SKIPPED'
932 + ]);
933 + } else {
934 + wp_send_json_error([
935 + 'retry' => true,
936 + 'image_url' => $url
937 + ]);
938 + }
939 + } else {
940 + $new_url = wp_get_attachment_url($new_image_id);
941 + $images_processed++;
942 + set_transient('elementor_import_images_processed', $images_processed, 60 * 60);
943 + $progress = round(($images_processed / $total_images) * 90) + 10;
944 +
945 + $this->replace_image_data($content, $url, $new_url, $current_image['id'], $new_image_id);
946 +
947 + set_transient('elementor_import_content', $content, 60 * 60);
948 +
949 + wp_send_json_success([
950 + 'progress' => $progress,
951 + 'message' => "Processed $images_processed of $total_images images.",
952 + 'image_url' => $url,
953 + 'new_image_url' => $new_url
954 + ]);
955 + }
956 + } else {
957 + // All images processed
958 + if ($create_new_page) {
959 + // Original behavior - create new page
960 + $new_post_id = wp_insert_post([
961 + 'post_title' => $page_title,
962 + 'post_content' => '',
963 + 'post_status' => 'publish',
964 + 'post_type' => 'page',
965 + ]);
966 +
967 + if ($new_post_id) {
968 + update_post_meta($new_post_id, '_elementor_data', wp_slash(json_encode($content)));
969 + update_post_meta($new_post_id, '_elementor_edit_mode', 'builder');
970 + update_post_meta($new_post_id, '_elementor_template_type', 'page');
971 + update_post_meta($new_post_id, '_elementor_version', $elementor_version);
972 +
973 + update_post_meta($new_post_id, '_wp_page_template', 'elementor_canvas');
974 +
975 + update_post_meta($new_post_id, '_wp_gutenberg_disable', '1');
976 + update_post_meta($new_post_id, '_wp_gutenberg_enabled', '0');
977 +
978 + delete_transient('elementor_import_content');
979 + delete_transient('elementor_import_images');
980 + delete_transient('elementor_import_total_images');
981 + delete_transient('elementor_import_images_processed');
982 + delete_transient('elementor_import_image_retry_count');
983 + delete_transient('elementor_import_page_title');
984 + delete_transient('elementor_import_elementor_version');
985 + delete_transient('elementor_import_existing_page_id');
986 + delete_transient('elementor_import_create_new_page');
987 +
988 + wp_send_json_success([
989 + 'message' => "Page imported successfully!",
990 + 'page_url' => get_permalink($new_post_id),
991 + ]);
992 + } else {
993 + wp_send_json_error('Failed to import page.');
994 + }
995 + } else {
996 + // New behavior - for existing page, just signal completion
997 + // Content and images are processed and stored in transients
998 + // Will be merged by king_addons_merge_with_existing_page endpoint
999 +
1000 + // error_log('King Addons Import: Image processing completed for existing page. Content ready for merge.');
1001 +
1002 + wp_send_json_success([
1003 + 'message' => "Image processing completed! Content ready for merge.",
1004 + 'images_processed' => $images_processed,
1005 + 'existing_page_id' => $existing_page_id,
1006 + 'processing_complete' => true
1007 + ]);
1008 + }
1009 + }
1010 +
1011 + if (time() >= $start_time + $timeout) {
1012 + wp_send_json_error('Process timeout, please resume the import.');
1013 + }
1014 + }
1015 +
1016 + public function download_image_to_media_gallery($image_url, $image_retry_count)
1017 + {
1018 + try {
1019 + if (!$this->is_safe_remote_url($image_url)) {
1020 + throw new Exception('Blocked potentially unsafe image URL: ' . $image_url);
1021 + }
1022 +
1023 + $response = wp_remote_get($image_url, ['timeout' => 30]);
1024 +
1025 + if (is_wp_error($response)) {
1026 + throw new Exception('HTTP request error: ' . $response->get_error_message());
1027 + }
1028 +
1029 + $status_code = wp_remote_retrieve_response_code($response);
1030 + if ($status_code !== 200) {
1031 + throw new Exception(
1032 + 'HTTP status code ' . $status_code . ' when trying to download: ' . $image_url
1033 + );
1034 + }
1035 +
1036 + $image_data = wp_remote_retrieve_body($response);
1037 + if (empty($image_data)) {
1038 + throw new Exception('Failed to retrieve image data from URL: ' . $image_url);
1039 + }
1040 +
1041 + $image_name = pathinfo(basename($image_url), PATHINFO_FILENAME);
1042 + $image_extension = pathinfo(basename($image_url), PATHINFO_EXTENSION);
1043 + $unique_image_name = $image_name . '-' . time() . '.' . $image_extension;
1044 +
1045 + $upload_dir = wp_upload_dir();
1046 + if (! file_exists($upload_dir['path'])) {
1047 + wp_mkdir_p($upload_dir['path']);
1048 + }
1049 + $image_file = $upload_dir['path'] . '/' . $unique_image_name;
1050 +
1051 + if (file_put_contents($image_file, $image_data) === false) {
1052 + throw new Exception('Failed to write image data to file: ' . $image_file);
1053 + }
1054 +
1055 + $wp_filetype = wp_check_filetype($unique_image_name);
1056 + $attachment = [
1057 + 'post_mime_type' => $wp_filetype['type'],
1058 + 'post_title' => sanitize_file_name($unique_image_name),
1059 + 'post_content' => '',
1060 + 'post_status' => 'inherit',
1061 + ];
1062 +
1063 + $attach_id = wp_insert_attachment($attachment, $image_file);
1064 +
1065 + // Conditionally disable intermediate image sizes based on setting
1066 + $improve_import = get_option('king_addons_improve_import_performance', '1');
1067 + if ($improve_import === '1') {
1068 + // Disable ALL extra sizes and big-image scaling
1069 + add_filter('intermediate_image_sizes', '__return_empty_array', 999);
1070 + add_filter('big_image_size_threshold', '__return_false', 999);
1071 +
1072 + /* Save only the original file info — no thumbnails, no EXIF */
1073 + // Get image dimensions
1074 + $image_size = getimagesize($image_file);
1075 + $width = $image_size ? $image_size[0] : 0;
1076 + $height = $image_size ? $image_size[1] : 0;
1077 +
1078 + $meta = [
1079 + 'file' => wp_basename($image_file),
1080 + 'filesize' => filesize($image_file),
1081 + 'width' => $width,
1082 + 'height' => $height,
1083 + 'sizes' => [],
1084 + 'image_meta' => [],
1085 + ];
1086 + wp_update_attachment_metadata($attach_id, $meta);
1087 +
1088 + // Clean up filters
1089 + remove_filter('intermediate_image_sizes', '__return_empty_array', 999);
1090 + remove_filter('big_image_size_threshold', '__return_false', 999);
1091 + } else {
1092 + if ($image_retry_count > 1) {
1093 + add_filter('intermediate_image_sizes', '__return_empty_array', 999);
1094 + }
1095 +
1096 + // Generate default metadata if optimization is off
1097 + require_once(ABSPATH . 'wp-admin/includes/image.php');
1098 + $attach_data = wp_generate_attachment_metadata($attach_id, $image_file);
1099 + wp_update_attachment_metadata($attach_id, $attach_data);
1100 +
1101 + if ($image_retry_count > 1) {
1102 + remove_filter('intermediate_image_sizes', '__return_empty_array', 999);
1103 + }
1104 + }
1105 +
1106 + return $attach_id;
1107 + } catch (Exception $e) {
1108 + // error_log('[KING_ADDONS_ERROR] ' . $e->getMessage());
1109 + return false;
1110 + }
1111 + }
1112 +
1113 + /**
1114 + * Basic SSRF guard for remote URLs.
1115 + *
1116 + * @param string $url URL to validate.
1117 + *
1118 + * @return bool
1119 + */
1120 + private function is_safe_remote_url(string $url): bool
1121 + {
1122 + $parsed = parse_url($url);
1123 + if (!$parsed || empty($parsed['scheme']) || empty($parsed['host'])) {
1124 + return false;
1125 + }
1126 +
1127 + if (!in_array($parsed['scheme'], ['http', 'https'], true)) {
1128 + return false;
1129 + }
1130 +
1131 + $host = $parsed['host'];
1132 + if (filter_var($host, FILTER_VALIDATE_IP)) {
1133 + return (bool) filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE);
1134 + }
1135 +
1136 + // Allow domain names; block obvious localhost patterns.
1137 + $lower = strtolower($host);
1138 + if ($lower === 'localhost' || str_ends_with($lower, '.local')) {
1139 + return false;
1140 + }
1141 +
1142 + return true;
1143 + }
1144 +
1145 + /**
1146 + * AJAX handler for getting sections catalog data
1147 + */
1148 + public function handle_get_sections_catalog(): void
1149 + {
1150 + if (!current_user_can('manage_options')) {
1151 + wp_send_json_error('Insufficient permissions');
1152 + return;
1153 + }
1154 +
1155 + // Check nonce - handle both admin area and popup
1156 + $nonce = isset($_POST['nonce']) ? sanitize_text_field($_POST['nonce']) : '';
1157 + $valid_nonce = $nonce && (wp_verify_nonce($nonce, 'kingAddonsNonce') ||
1158 + wp_verify_nonce($nonce, 'king_addons_template_catalog'));
1159 +
1160 + if (!$valid_nonce) {
1161 + wp_send_json_error('Invalid nonce');
1162 + return;
1163 + }
1164 +
1165 + if (!class_exists('King_Addons\\SectionsMap')) {
1166 + require_once KING_ADDONS_PATH . 'includes/SectionsMap.php';
1167 + }
1168 +
1169 + $sections_map_class = '\\King_Addons\\SectionsMap';
1170 + $sections_map = is_callable([$sections_map_class, 'getSectionsMapArray'])
1171 + ? call_user_func([$sections_map_class, 'getSectionsMapArray'])
1172 + : [];
1173 + $sections = $sections_map['sections'] ?? [];
1174 +
1175 + $is_premium_active = function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code();
1176 +
1177 + // Get filters from request
1178 + $search_query = sanitize_text_field($_POST['search'] ?? '');
1179 + $selected_category = sanitize_text_field($_POST['category'] ?? '');
1180 + $selected_type = sanitize_text_field($_POST['section_type'] ?? '');
1181 + $selected_plan = sanitize_text_field($_POST['plan'] ?? '');
1182 + $current_page = max(1, intval($_POST['page'] ?? 1));
1183 +
1184 + // Get categories and section types for filters
1185 + $categories = [];
1186 + $section_types = [];
1187 +
1188 + foreach ($sections as $section_key => $section) {
1189 + if (!in_array($section['category'], $categories)) {
1190 + $categories[] = $section['category'];
1191 + }
1192 + if (!in_array($section['section_type'], $section_types)) {
1193 + $section_types[] = $section['section_type'];
1194 + }
1195 + }
1196 +
1197 + sort($categories);
1198 + sort($section_types);
1199 +
1200 + // Filter sections
1201 + $filtered_sections = [];
1202 +
1203 + foreach ($sections as $section_key => $section) {
1204 + // Add section key for frontend
1205 + $section['section_key'] = $section_key;
1206 +
1207 + // Note: Premium sections are shown but restricted for import if no premium license
1208 +
1209 + // Apply search filter
1210 + if (!empty($search_query)) {
1211 + $found_in_title = stripos($section['title'], $search_query) !== false;
1212 + $found_in_tags = false;
1213 +
1214 + foreach ($section['tags'] ?? [] as $tag) {
1215 + if (stripos($tag, $search_query) !== false) {
1216 + $found_in_tags = true;
1217 + break;
1218 + }
1219 + }
1220 +
1221 + if (!$found_in_title && !$found_in_tags) {
1222 + continue;
1223 + }
1224 + }
1225 +
1226 + // Apply category filter
1227 + if (!empty($selected_category) && $section['category'] !== $selected_category) {
1228 + continue;
1229 + }
1230 +
1231 + // Apply section type filter
1232 + if (!empty($selected_type) && $section['section_type'] !== $selected_type) {
1233 + continue;
1234 + }
1235 +
1236 + // Apply plan filter
1237 + if (!empty($selected_plan) && $section['plan'] !== $selected_plan) {
1238 + continue;
1239 + }
1240 +
1241 + $filtered_sections[] = $section;
1242 + }
1243 +
1244 + // Pagination
1245 + $items_per_page = 20;
1246 + $total_sections = count($filtered_sections);
1247 + $total_pages = ceil($total_sections / $items_per_page);
1248 + $offset = ($current_page - 1) * $items_per_page;
1249 + $paged_sections = array_slice($filtered_sections, $offset, $items_per_page);
1250 +
1251 + wp_send_json_success([
1252 + 'sections' => $paged_sections,
1253 + 'categories' => $categories,
1254 + 'section_types' => $section_types,
1255 + 'pagination' => [
1256 + 'current_page' => $current_page,
1257 + 'total_pages' => $total_pages,
1258 + 'total_sections' => $total_sections,
1259 + 'items_per_page' => $items_per_page
1260 + ],
1261 + 'is_premium_active' => $is_premium_active
1262 + ]);
1263 + }
1264 +
1265 + /**
1266 + * AJAX handler for importing section to current page (from main admin catalog)
1267 + */
1268 + public function handle_import_section_to_page(): void
1269 + {
1270 + if (!current_user_can('edit_posts')) {
1271 + wp_send_json_error('Insufficient permissions');
1272 + return;
1273 + }
1274 +
1275 + if (!wp_verify_nonce($_POST['nonce'], 'kingAddonsNonce')) {
1276 + wp_send_json_error('Invalid nonce');
1277 + return;
1278 + }
1279 +
1280 + $section_key = sanitize_text_field($_POST['section_key']);
1281 + $section_plan = sanitize_text_field($_POST['section_plan']);
1282 + $is_premium_active = function_exists('king_addons_freemius') && king_addons_freemius()->can_use_premium_code();
1283 +
1284 + // Determine API URL and install ID (same logic as templates)
1285 + if ($is_premium_active && $section_plan === 'premium') {
1286 + $api_url = 'https://api.kingaddons.com/get-section.php';
1287 +
1288 + // Use the same method as original templates catalog
1289 + if (function_exists('king_addons_freemius')) {
1290 + $freemius_site = king_addons_freemius()->get_site();
1291 + $install_id = $freemius_site ? $freemius_site->id : 0;
1292 + } else {
1293 + $install_id = 0;
1294 + }
1295 +
1296 + // error_log('King Addons Premium Section: Using install_id: ' . $install_id . ' for premium section: ' . $section_key);
1297 + } elseif ($section_plan === 'free') {
1298 + $api_url = 'https://api.kingaddons.com/get-section-free.php';
1299 + $install_id = 0;
1300 + // error_log('King Addons Free Section: Fetching free section: ' . $section_key);
1301 + } else {
1302 + // error_log('King Addons Section Error: Premium section requires premium license. Section: ' . $section_key . ', Plan: ' . $section_plan . ', Premium Active: ' . ($is_premium_active ? 'Yes' : 'No'));
1303 + wp_send_json_error('Premium section requires premium license');
1304 + return;
1305 + }
1306 +
1307 + // Get section data from API (same as templates)
1308 + $response = wp_remote_post($api_url, [
1309 + 'headers' => ['Content-Type' => 'application/json'],
1310 + 'body' => json_encode([
1311 + 'key' => $section_key,
1312 + 'install' => $install_id,
1313 + ]),
1314 + 'timeout' => 60
1315 + ]);
1316 +
1317 + if (is_wp_error($response)) {
1318 + wp_send_json_error('Failed to fetch section: ' . $response->get_error_message());
1319 + return;
1320 + }
1321 +
1322 + $body = wp_remote_retrieve_body($response);
1323 + $data = json_decode($body, true);
1324 +
1325 + // error_log('King Addons Section API Response: ' . substr($body, 0, 500) . (strlen($body) > 500 ? '...' : ''));
1326 +
1327 + if (!$data) {
1328 + // error_log('King Addons Section Error: Failed to decode JSON response');
1329 + wp_send_json_error('Invalid JSON response from section API');
1330 + return;
1331 + }
1332 +
1333 + if (!isset($data['success']) || !$data['success']) {
1334 + $error_message = isset($data['message']) ? $data['message'] : 'Unknown API error';
1335 + // error_log('King Addons Section Error: API returned error: ' . $error_message);
1336 + wp_send_json_error('Section API error: ' . $error_message);
1337 + return;
1338 + }
1339 +
1340 + // Return section data for frontend processing (adjust format from your API)
1341 + wp_send_json_success([
1342 + 'section_data' => $data['section'], // Your API returns 'section' not 'landing'
1343 + 'message' => 'Section data retrieved successfully'
1344 + ]);
1345 + }
1346 +
1347 + public function handle_get_collection_details()
1348 + {
1349 + // Verify nonce
1350 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'kingAddonsNonce')) {
1351 + wp_send_json_error('Invalid nonce');
1352 + }
1353 +
1354 + $collection_id = isset($_POST['collection_id']) ? sanitize_text_field($_POST['collection_id']) : '';
1355 + if (empty($collection_id)) {
1356 + wp_send_json_error('Collection ID is required');
1357 + }
1358 +
1359 + $templates = TemplatesMap::getTemplatesMapArray();
1360 +
1361 + if (!class_exists('King_Addons\\SectionsMap')) {
1362 + require_once KING_ADDONS_PATH . 'includes/SectionsMap.php';
1363 + }
1364 + $sections_map = SectionsMap::getSectionsMapArray();
1365 + $sections = $sections_map['sections'] ?? [];
1366 +
1367 + // Filter Templates
1368 + $collection_templates = [];
1369 + $template_keys = [];
1370 + foreach ($templates['templates'] as $key => $template) {
1371 + if (isset($template['collection']) && (string)$template['collection'] === (string)$collection_id) {
1372 + $template['template_key'] = $key;
1373 + $collection_templates[] = $template;
1374 + $template_keys[] = $key;
1375 + }
1376 + }
1377 +
1378 + // Filter Sections
1379 + $collection_sections = [];
1380 + foreach ($sections as $key => $section) {
1381 + $parent = $section['parent_template'] ?? '';
1382 + if ($parent && in_array($parent, $template_keys)) {
1383 + $section['section_key'] = $key;
1384 + $collection_sections[] = $section;
1385 + }
1386 + }
1387 +
1388 + // Render Templates HTML
1389 + ob_start();
1390 + if (empty($collection_templates)) {
1391 + echo '<p class="templates-not-found">' . esc_html__('No templates found in this collection.', 'king-addons') . '</p>';
1392 + } else {
1393 + foreach ($collection_templates as $template) {
1394 + $attr_key = $template['template_key'];
1395 + ?>
1396 + <div class="template-item"
1397 + data-category="<?php echo esc_attr($template['category']); ?>"
1398 + data-tags="<?php echo esc_attr(implode(',', $template['tags'])); ?>"
1399 + data-template-key="<?php echo esc_attr($attr_key); ?>"
1400 + data-template-plan="<?php echo esc_attr($template['plan']); ?>">
1401 + <img class="kng-addons-template-thumbnail" loading="lazy"
1402 + src="<?php echo esc_url("https://thumbnails.kingaddons.com/$attr_key.png?v=4"); ?>"
1403 + alt="<?php echo esc_attr($template['title']); ?>">
1404 + <h3><?php echo esc_html($template['title']); ?></h3>
1405 + <div class="template-plan template-plan-<?php echo esc_html($template['plan']); ?>"><?php echo esc_html($template['plan']); ?></div>
1406 + </div>
1407 + <?php
1408 + }
1409 + }
1410 + $templates_html = ob_get_clean();
1411 +
1412 + // Render Sections HTML
1413 + ob_start();
1414 + if (empty($collection_sections)) {
1415 + echo '<p class="sections-not-found">' . esc_html__('No sections found in this collection.', 'king-addons') . '</p>';
1416 + } else {
1417 + foreach ($collection_sections as $section) {
1418 + $section_key = $section['section_key'];
1419 + $plan = $section['plan'];
1420 + $title = $section['title'];
1421 + $thumb_url = "https://thumbnails.kingaddons.com/sections/$plan/$section_key.png";
1422 + ?>
1423 + <div class="section-item"
1424 + data-section-key="<?php echo esc_attr($section_key); ?>"
1425 + data-section-plan="<?php echo esc_attr($plan); ?>">
1426 + <div class="section-preview">
1427 + <img src="<?php echo esc_url($thumb_url); ?>" alt="<?php echo esc_attr($title); ?>" loading="lazy">
1428 + <div class="section-actions">
1429 + <button class="import-section-btn" data-section-key="<?php echo esc_attr($section_key); ?>" data-section-plan="<?php echo esc_attr($plan); ?>">
1430 + <i class="eicon-file-download"></i> <?php esc_html_e('Import', 'king-addons'); ?>
1431 + </button>
1432 + <a href="<?php echo esc_url('https://sections.kingaddons.com/' . $section_key); ?>" class="live-preview-btn" target="_blank">
1433 + <i class="eicon-preview-medium"></i> <?php esc_html_e('Live Preview', 'king-addons'); ?>
1434 + </a>
1435 + </div>
1436 + <?php if ($plan === 'premium'): ?>
1437 + <span class="section-badge-pro">PRO</span>
1438 + <?php endif; ?>
1439 + </div>
1440 + <div class="section-info">
1441 + <h4><?php echo esc_html($title); ?></h4>
1442 + </div>
1443 + </div>
1444 + <?php
1445 + }
1446 + }
1447 + $sections_html = ob_get_clean();
1448 +
1449 + wp_send_json_success([
1450 + 'templates_html' => $templates_html,
1451 + 'sections_html' => $sections_html,
1452 + 'templates_count' => count($collection_templates),
1453 + 'sections_count' => count($collection_sections)
1454 + ]);
1455 + }
1456 +
1457 + public function handle_get_collections_rows()
1458 + {
1459 + if (!current_user_can('manage_options')) {
1460 + wp_send_json_error('Insufficient permissions');
1461 + }
1462 +
1463 + // Verify nonce
1464 + if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'kingAddonsNonce')) {
1465 + wp_send_json_error('Invalid nonce');
1466 + }
1467 +
1468 + $page = isset($_POST['page']) ? max(1, intval($_POST['page'])) : 1;
1469 + $search = isset($_POST['search']) ? sanitize_text_field(wp_unslash($_POST['search'])) : '';
1470 + $per_page = 10; // Number of collections per page
1471 +
1472 + $templates = TemplatesMap::getTemplatesMapArray();
1473 + $collections_map = CollectionsMap::getCollectionsMapArray();
1474 +
1475 + // Load Sections to compute section counts per collection
1476 + if (!class_exists('King_Addons\\SectionsMap')) {
1477 + require_once KING_ADDONS_PATH . 'includes/SectionsMap.php';
1478 + }
1479 + $sections_map = SectionsMap::getSectionsMapArray();
1480 + $sections = $sections_map['sections'] ?? [];
1481 +
1482 + // Build templates grouped by collection + counts
1483 + // Search behavior mirrors template search: title + tags (and direct key).
1484 + $search_terms = array_values(array_filter(preg_split('/\s+/', (string) $search)));
1485 + $has_search = !empty($search_terms);
1486 +
1487 + $templates_by_collection = [];
1488 + $templates_by_collection_title = [];
1489 + $templates_by_collection_tags = [];
1490 +
1491 + $template_counts = [];
1492 + $template_key_to_collection = [];
1493 + $template_matches_search = [];
1494 + $collections_matching_by_name = [];
1495 +
1496 + $contains_ci = static function ($haystack, $needle): bool {
1497 + if ($needle === '') {
1498 + return true;
1499 + }
1500 + if (function_exists('mb_stripos')) {
1501 + return mb_stripos((string) $haystack, (string) $needle) !== false;
1502 + }
1503 + return stripos((string) $haystack, (string) $needle) !== false;
1504 + };
1505 +
1506 + if ($has_search) {
1507 + foreach ($collections_map as $collection_id => $collection_name) {
1508 + foreach ($search_terms as $term) {
1509 + if ($term !== '' && $contains_ci($collection_name, $term)) {
1510 + $collections_matching_by_name[$collection_id] = true;
1511 + break;
1512 + }
1513 + }
1514 + }
1515 + }
1516 +
1517 + foreach (($templates['templates'] ?? []) as $key => $template) {
1518 + $collection_id = $template['collection'] ?? '';
1519 + if ($collection_id === '' || !isset($collections_map[$collection_id])) {
1520 + continue;
1521 + }
1522 +
1523 + $template_key_to_collection[$key] = $collection_id;
1524 +
1525 + if (!isset($template_counts[$collection_id])) {
1526 + $template_counts[$collection_id] = 0;
1527 + }
1528 + $template_counts[$collection_id]++;
1529 +
1530 + $title = (string) ($template['title'] ?? '');
1531 + $tags = is_array($template['tags'] ?? null) ? $template['tags'] : [];
1532 + $matches_by_title = false;
1533 + $matches_by_tags = false;
1534 + $matches_by_key = ($search !== '' && $key === $search);
1535 +
1536 + if ($has_search) {
1537 + foreach ($search_terms as $term) {
1538 + if ($term === '') {
1539 + continue;
1540 + }
1541 +
1542 + if ($contains_ci($title, $term)) {
1543 + $matches_by_title = true;
1544 + continue;
1545 + }
1546 +
1547 + if ($contains_ci($key, $term)) {
1548 + $matches_by_key = true;
1549 + continue;
1550 + }
1551 +
1552 + foreach ($tags as $tag) {
1553 + if ($contains_ci((string) $tag, $term)) {
1554 + $matches_by_tags = true;
1555 + break;
1556 + }
1557 + }
1558 + }
1559 + }
1560 +
1561 + $collection_name_match = $has_search && isset($collections_matching_by_name[$collection_id]);
1562 + $template_is_relevant = !$has_search || $collection_name_match || $matches_by_key || $matches_by_title || $matches_by_tags;
1563 + $template_matches_search[$key] = (bool) ($matches_by_key || $matches_by_title || $matches_by_tags);
1564 +
1565 + if (!$template_is_relevant) {
1566 + continue;
1567 + }
1568 +
1569 + $item = [
1570 + 'key' => $key,
1571 + 'title' => $title,
1572 + 'plan' => $template['plan'],
1573 + 'category' => $template['category'],
1574 + ];
1575 +
1576 + if (!isset($templates_by_collection_title[$collection_id])) {
1577 + $templates_by_collection_title[$collection_id] = [];
1578 + }
1579 + if (!isset($templates_by_collection_tags[$collection_id])) {
1580 + $templates_by_collection_tags[$collection_id] = [];
1581 + }
1582 +
1583 + // No search: keep natural order (default Collections landing should not be empty).
1584 + if (!$has_search) {
1585 + if (!isset($templates_by_collection[$collection_id])) {
1586 + $templates_by_collection[$collection_id] = [];
1587 + }
1588 + $templates_by_collection[$collection_id][] = $item;
1589 + continue;
1590 + }
1591 +
1592 + // If the collection name matches, keep natural order; otherwise prioritize title matches.
1593 + if ($collection_name_match) {
1594 + if (!isset($templates_by_collection[$collection_id])) {
1595 + $templates_by_collection[$collection_id] = [];
1596 + }
1597 + $templates_by_collection[$collection_id][] = $item;
1598 + } else {
1599 + if ($matches_by_key || $matches_by_title) {
1600 + $templates_by_collection_title[$collection_id][] = $item;
1601 + } elseif ($matches_by_tags) {
1602 + $templates_by_collection_tags[$collection_id][] = $item;
1603 + }
1604 + }
1605 + }
1606 +
1607 + // Merge per-collection buckets and apply soft limit
1608 + $collection_ids_all = array_keys($template_counts);
1609 + foreach ($collection_ids_all as $collection_id) {
1610 + if (!isset($templates_by_collection[$collection_id])) {
1611 + $templates_by_collection[$collection_id] = [];
1612 + }
1613 +
1614 + if ($has_search && !isset($collections_matching_by_name[$collection_id])) {
1615 + $merged = array_merge(
1616 + $templates_by_collection_title[$collection_id] ?? [],
1617 + $templates_by_collection_tags[$collection_id] ?? []
1618 + );
1619 + $templates_by_collection[$collection_id] = $merged;
1620 + }
1621 +
1622 + if (count($templates_by_collection[$collection_id]) > 20) {
1623 + $templates_by_collection[$collection_id] = array_slice($templates_by_collection[$collection_id], 0, 20);
1624 + }
1625 + }
1626 +
1627 + // Compute sections count per collection using parent_template -> template -> collection mapping.
1628 + // When searching, count only sections whose parent template matches search (unless collection name matches).
1629 + $section_counts = [];
1630 + foreach ($sections as $section) {
1631 + $parent = $section['parent_template'] ?? '';
1632 + if (!$parent || !isset($template_key_to_collection[$parent])) {
1633 + continue;
1634 + }
1635 +
1636 + $collection_id = $template_key_to_collection[$parent];
1637 + $collection_name_match = $has_search && isset($collections_matching_by_name[$collection_id]);
1638 + if ($has_search && !$collection_name_match) {
1639 + if (!isset($template_matches_search[$parent]) || !$template_matches_search[$parent]) {
1640 + continue;
1641 + }
1642 + }
1643 +
1644 + if (!isset($section_counts[$collection_id])) {
1645 + $section_counts[$collection_id] = 0;
1646 + }
1647 + $section_counts[$collection_id]++;
1648 + }
1649 +
1650 + // Only collections that actually have templates in the (possibly filtered) result
1651 + $collection_ids = array_keys(array_filter($templates_by_collection, static function ($items) {
1652 + return !empty($items);
1653 + }));
1654 +
1655 + // Sort collections by name to match the main Collections view
1656 + usort($collection_ids, function ($a, $b) use ($collections_map) {
1657 + return strcasecmp((string) ($collections_map[$a] ?? ''), (string) ($collections_map[$b] ?? ''));
1658 + });
1659 +
1660 + $total_collections = count($collection_ids);
1661 + $total_pages = (int) ceil($total_collections / $per_page);
1662 + $page = min($page, max(1, $total_pages));
1663 + $offset = ($page - 1) * $per_page;
1664 + $paged_collection_ids = array_slice($collection_ids, $offset, $per_page);
1665 +
1666 + $result_collections = [];
1667 + foreach ($paged_collection_ids as $id) {
1668 + $result_collections[] = [
1669 + 'id' => $id,
1670 + 'name' => $collections_map[$id],
1671 + 'templates' => $templates_by_collection[$id],
1672 + 'templates_count' => (int) ($template_counts[$id] ?? 0),
1673 + 'sections_count' => (int) ($section_counts[$id] ?? 0)
1674 + ];
1675 + }
1676 +
1677 + wp_send_json_success([
1678 + 'collections' => $result_collections,
1679 + 'pagination' => [
1680 + 'total' => $total_collections,
1681 + 'pages' => $total_pages,
1682 + 'current' => $page
1683 + ]
1684 + ]);
1685 + }
1686 +}
1687 +
1688 +Templates::instance();