PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.2
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.2
4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 All 200 releases
← All changes | includes/Editors/Elementor.php +256 -59 3.4.24.9.2 View file →
@@ -2,9 +2,9 @@
2 2
3 3 namespace WPDeveloper\BetterDocs\Editors;
4 4
5 5 if ( ! defined( 'ABSPATH' ) ) {
6 - exit; // Exit if accessed directly
6 + exit; // Exit if accessed directly
7 7 }
8 8
9 9 use Elementor\Controls_Manager;
10 10 use Elementor\Api as ElementorAPI;
@@ -13,8 +13,9 @@
13 13 use WPDeveloper\BetterDocs\Utils\Enqueue;
14 14 use WPDeveloper\BetterDocs\Editors\Elementor\Helper;
15 15 use WPDeveloper\BetterDocs\Editors\Elementor\SingleDocs;
16 16 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\ToC;
17 +use WPDeveloper\BetterDocs\Editors\Elementor\Widget\DocAuthor;
17 18 use WPDeveloper\BetterDocs\Editors\Elementor\DocsArchive;
18 19 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Title;
19 20 use WPDeveloper\BetterDocs\Editors\Elementor\Tags\TitleTag;
20 21 use WPDeveloper\BetterDocs\Editors\Elementor\TemplateSource;
@@ -25,14 +26,22 @@
25 26 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Feedback;
26 27 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Basic\FAQ;
27 28 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Reactions;
28 29 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Navigation;
30 +use WPDeveloper\BetterDocs\Editors\Elementor\Widget\CodeSnippet;
31 +use WPDeveloper\BetterDocs\Editors\Elementor\Widget\CodeSnippetTab;
29 32 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\ArchiveList;
33 +use WPDeveloper\BetterDocs\Editors\Elementor\Widget\PrintButton;
30 34 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Breadcrumbs;
35 +use WPDeveloper\BetterDocs\Editors\Elementor\Widget\ReadingTime;
36 +use WPDeveloper\BetterDocs\Editors\Elementor\Widget\ArticleSummary;
31 37 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Basic\SearchForm;
32 38 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Basic\CategoryBox;
33 39 use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Basic\CategoryGrid;
40 +use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Basic\CategorySlateLayout;
34 41 use WPDeveloper\BetterDocs\Editors\Elementor\Conditions\ArchiveCondition;
42 +use WPDeveloper\BetterDocs\Editors\Elementor\Widget\Tags;
43 +use WPDeveloper\BetterDocs\Editors\Elementor\Widget\CategoryArchiveHeader;
35 44
36 45 class Elementor extends BaseEditor {
37 46 protected $is_elementor_active = false;
38 47 protected $is_elementor_pro_active = false;
@@ -78,8 +87,9 @@
78 87 add_filter( 'elementor/editor/localize_settings', [$this, 'promote_pro_elements'] );
79 88
80 89 add_filter( 'elementor/theme/need_override_location', [$this, 'override_location'], 10, 2 );
81 90 add_action( 'betterdocs/elementor/widgets/query', [$this, 'betterdocs_query'], 10, 2 );
91 + add_filter( 'betterdocs_base_terms_args', [$this, 'handle_editor_mode_terms_args'], 10, 1 );
82 92
83 93 if ( $this->is_elementor_pro_active ) {
84 94 add_action( 'elementor/dynamic_tags/register', [$this, 'register_basic_tags'] );
85 95 add_action( 'elementor/widgets/register', [$this, 'register_theme_builder_widgets'] );
@@ -195,9 +205,11 @@
195 205 } else if ( $wb->get_name() !== 'betterdocs-sidebar' ) {
196 206 $wb->add_control(
197 207 'box_per_page',
198 208 [
199 - 'label' => __( 'Box Per Page', 'betterdocs' ),
209 + 'label' => $wb->get_name() == 'betterdocs-tab-view-list'
210 + ? __( 'Tabs Per Page', 'betterdocs' )
211 + : __( 'Box Per Page', 'betterdocs' ),
200 212 'type' => Controls_Manager::NUMBER,
201 213 'default' => '8'
202 214 ]
203 215 );
@@ -216,9 +228,9 @@
216 228 if ( $wb->get_name() === 'betterdocs-sidebar' ) {
217 229 $wb->add_control(
218 230 'orderby',
219 231 [
220 - 'label' => __( 'Order By', 'betterdocs' ),
232 + 'label' => __( 'Terms Order By', 'betterdocs' ),
221 233 'type' => Controls_Manager::SELECT,
222 234 'options' => [
223 235 'none' => __( 'No order', 'betterdocs' ),
224 236 'name' => __( 'Name', 'betterdocs' ),
@@ -236,9 +248,9 @@
236 248
237 249 $wb->add_control(
238 250 'order',
239 251 [
240 - 'label' => __( 'Order', 'betterdocs' ),
252 + 'label' => __( 'Terms Order', 'betterdocs' ),
241 253 'type' => Controls_Manager::SELECT,
242 254 'options' => [
243 255 'ASC' => 'Ascending',
244 256 'DESC' => 'Descending'
@@ -249,8 +261,34 @@
249 261 ]
250 262
251 263 ]
252 264 );
265 +
266 + $wb->add_control(
267 + 'post_orderby',
268 + [
269 + 'label' => __( 'Docs Order By', 'betterdocs' ),
270 + 'type' => Controls_Manager::SELECT,
271 + 'options' => $this->helper->orderby_options(),
272 + 'default' => 'date'
273 + ]
274 + );
275 +
276 + $wb->add_control(
277 + 'post_order',
278 + [
279 + 'label' => __( 'Docs Order', 'betterdocs' ),
280 + 'type' => Controls_Manager::SELECT,
281 + 'options' => [
282 + 'asc' => 'Ascending',
283 + 'desc' => 'Descending'
284 + ],
285 + 'default' => 'asc',
286 + 'condition' => [
287 + 'post_orderby!' => 'betterdocs_order'
288 + ]
289 + ]
290 + );
253 291 } else {
254 292 $wb->add_control(
255 293 'orderby',
256 294 [
@@ -279,9 +317,9 @@
279 317 'options' => [
280 318 'ASC' => 'Ascending',
281 319 'DESC' => 'Descending'
282 320 ],
283 - 'default' => 'asc',
321 + 'default' => 'ASC',
284 322 'condition' => [
285 323 'orderby!' => 'betterdocs_order'
286 324 ]
287 325
@@ -288,9 +326,9 @@
288 326 ]
289 327 );
290 328 }
291 329
292 - if ( $wb->get_name() === 'betterdocs-category-grid' ) {
330 + if ( $wb->get_name() === 'betterdocs-category-grid' || $wb->get_name() === 'betterdocs-category-slate-layout' ) {
293 331 $wb->add_control(
294 332 'grid_posts_query_heading',
295 333 [
296 334 'label' => __( 'Grid List Posts', 'betterdocs' ),
@@ -313,9 +351,9 @@
313 351 [
314 352 'label' => __( 'Order By', 'betterdocs' ),
315 353 'type' => Controls_Manager::SELECT,
316 354 'options' => $this->helper->orderby_options(),
317 - 'default' => 'date'
355 + 'default' => 'betterdocs_order'
318 356 ]
319 357 );
320 358
321 359 $wb->add_control(
@@ -326,38 +364,103 @@
326 364 'options' => [
327 365 'asc' => 'Ascending',
328 366 'desc' => 'Descending'
329 367 ],
330 - 'default' => 'desc'
368 + 'default' => 'asc',
369 + 'condition' => [
370 + 'post_orderby!' => 'betterdocs_order'
371 + ]
331 372 ]
332 373 );
374 + }
333 375
376 + if ( $wb->get_name() === 'betterdocs-category-grid' ) {
377 + $wb->add_control(
378 + 'nested_subcategory',
379 + [
380 + 'label' => __( 'Enable Nested Subcategory', 'betterdocs' ),
381 + 'type' => Controls_Manager::SWITCHER,
382 + 'label_on' => __( 'Yes', 'betterdocs' ),
383 + 'label_off' => __( 'No', 'betterdocs' ),
384 + 'return_value' => 'true',
385 + 'default' => false
386 + ]
387 + );
388 +
389 + $wb->add_control(
390 + 'subcategory_per_grid',
391 + [
392 + 'label' => __( 'Subcategory Per Grid', 'betterdocs' ),
393 + 'type' => Controls_Manager::NUMBER,
394 + 'default' => '2',
395 + 'condition' => [
396 + 'nested_subcategory' => 'true'
397 + ]
398 + ]
399 + );
400 +
401 + $wb->add_control(
402 + 'post_per_subcat',
403 + [
404 + 'label' => __( 'Post Per Subcategory', 'betterdocs' ),
405 + 'type' => Controls_Manager::NUMBER,
406 + 'default' => '6',
407 + 'condition' => [
408 + 'nested_subcategory' => 'true'
409 + ]
410 + ]
411 + );
412 +
413 + $wb->add_control(
414 + 'lazy_load',
415 + [
416 + 'label' => __( 'Lazy Load Docs & Subcategories', 'betterdocs' ),
417 + 'description' => __( 'Render only the active category upfront; fetch the rest on click. Recommended for large knowledge bases when this widget is used as a sidebar.', 'betterdocs' ),
418 + 'type' => Controls_Manager::SWITCHER,
419 + 'label_on' => __( 'Yes', 'betterdocs' ),
420 + 'label_off' => __( 'No', 'betterdocs' ),
421 + 'return_value' => 'true',
422 + 'default' => ''
423 + ]
424 + );
425 + }
426 +
427 + if ( $wb->get_name() === 'betterdocs-category-box' ) {
334 428 $wb->add_control(
335 429 'nested_subcategory',
336 430 [
337 - 'label' => __( 'Enable Nested Subcategory', 'betterdocs' ),
431 + 'label' => __( 'Nested Subcategory', 'betterdocs' ),
338 432 'type' => Controls_Manager::SWITCHER,
339 433 'label_on' => __( 'Yes', 'betterdocs' ),
340 434 'label_off' => __( 'No', 'betterdocs' ),
341 435 'return_value' => 'true',
342 - 'default' => false
436 + 'default' => 'true',
343 437 ]
344 438 );
345 439
346 - $wb->add_control(
347 - 'post_per_subcat',
440 + $wb->add_control(
441 + 'show_dynamic_categories_in_doc_category_template',
348 442 [
349 - 'label' => __( 'Post Per Subcategory', 'betterdocs' ),
350 - 'type' => Controls_Manager::NUMBER,
351 - 'default' => '6',
352 - 'condition' => [
353 - 'nested_subcategory' => 'true'
354 - ]
443 + 'label' => __( 'Show Categories Dynamically', 'betterdocs' ),
444 + 'type' => Controls_Manager::SWITCHER,
445 + 'label_on' => __( 'Yes', 'betterdocs' ),
446 + 'label_off' => __( 'No', 'betterdocs' ),
447 + 'return_value' => 'true',
448 + 'default' => 'true',
355 449 ]
356 450 );
451 +
452 + $wb->add_control(
453 + 'show_dynamic_categories_in_doc_category_template_description',
454 + [
455 + 'type' => Controls_Manager::RAW_HTML,
456 + 'raw' => __( "Note: This option is only applicable for BetterDocs Category Archive page. If enabled, this will show the Categories dynamically in your BetterDocs Category Archive page.", 'betterdocs' ),
457 + 'content_classes' => 'elementor-panel-alert elementor-panel-alert-info'
458 + ]
459 + );
357 460 }
358 461
359 - if ( $wb->get_name() === 'betterdocs-category-box' ) {
462 + if ( $wb->get_name() === 'betterdocs-sidebar' ) {
360 463 $wb->add_control(
361 464 'nested_subcategory',
362 465 [
363 466 'label' => __( 'Nested Subcategory', 'betterdocs' ),
@@ -364,23 +467,22 @@
364 467 'type' => Controls_Manager::SWITCHER,
365 468 'label_on' => __( 'Yes', 'betterdocs' ),
366 469 'label_off' => __( 'No', 'betterdocs' ),
367 470 'return_value' => 'true',
368 - 'default' => false
471 + 'default' => ( $this->settings->get( 'archive_nested_subcategory' ) == 1 ) ? 'true' : ''
369 472 ]
370 473 );
371 - }
372 474
373 - if ( $wb->get_name() === 'betterdocs-sidebar' ) {
374 475 $wb->add_control(
375 - 'nested_subcategory',
476 + 'lazy_load',
376 477 [
377 - 'label' => __( 'Nested Subcategory', 'betterdocs' ),
478 + 'label' => __( 'Lazy Load Docs & Subcategories', 'betterdocs' ),
479 + 'description' => __( 'Render only the active category upfront; fetch the rest on click (or on scroll for the Memphis sidebar layout).', 'betterdocs' ),
378 480 'type' => Controls_Manager::SWITCHER,
379 481 'label_on' => __( 'Yes', 'betterdocs' ),
380 482 'label_off' => __( 'No', 'betterdocs' ),
381 483 'return_value' => 'true',
382 - 'default' => ( $this->settings->get( 'archive_nested_subcategory' ) == 1 ) ? 'true' : ''
484 + 'default' => ( $this->settings->get( 'archive_lazy_load_descendants' ) == 1 ) ? 'true' : ''
383 485 ]
384 486 );
385 487 }
386 488
@@ -394,8 +496,17 @@
394 496 ]
395 497 );
396 498
397 499 $wb->add_control(
500 + 'tab_box_per_tabs',
501 + [
502 + 'label' => __( 'Box Per Tabs', 'betterdocs' ),
503 + 'type' => Controls_Manager::NUMBER,
504 + 'default' => '10'
505 + ]
506 + );
507 +
508 + $wb->add_control(
398 509 'post_per_tab',
399 510 [
400 511 'label' => __( 'Posts Per Tab', 'betterdocs' ),
401 512 'type' => Controls_Manager::NUMBER,
@@ -511,16 +622,25 @@
511 622 return '';
512 623 }
513 624
514 625 public function get_kb_terms( $prettify = false, $term_id = true ) {
515 - $args = [
516 - 'taxonomy' => 'knowledge_base',
517 - 'hide_empty' => true,
518 - 'parent' => 0
519 - ];
626 + // This list populates the widget "Knowledge Bases" selector — an admin-side
627 + // config picker, not a front-end listing. It must show EVERY KB regardless of
628 + // whether it currently has content or is access-restricted for the editing
629 + // user's role. Previously it ran through terms_query()/get_terms(), so on a
630 + // regular page the Pro access-control terms_clauses filter + hide_empty pruned
631 + // the list to a subset, while the theme-builder context showed them all.
632 + // hide_empty:false keeps empty KBs; suppress_filters:true signals AccessControl
633 + // (get_terms_args) to skip its role-based terms_clauses filter, so the picker is
634 + // consistent everywhere.
635 + $terms = get_terms( [
636 + 'taxonomy' => 'knowledge_base',
637 + 'hide_empty' => false,
638 + 'parent' => 0,
639 + 'orderby' => 'name',
640 + 'suppress_filters' => true,
641 + ] );
520 642
521 - $terms = get_terms( $args );
522 -
523 643 if ( is_wp_error( $terms ) ) {
524 644 return [];
525 645 }
526 646
@@ -527,9 +647,11 @@
527 647 if ( $prettify ) {
528 648 $pretty_taxonomies = [];
529 649
530 650 foreach ( $terms as $term ) {
531 - $pretty_taxonomies[$term_id ? $term->term_id : $term->slug] = $term->name;
651 + // Decode HTML entities (e.g. "&" → "&") so the select2 control
652 + // shows the real KB name instead of the escaped entity.
653 + $pretty_taxonomies[$term_id ? $term->term_id : $term->slug] = wp_specialchars_decode( $term->name, ENT_QUOTES );
532 654 }
533 655
534 656 return $pretty_taxonomies;
535 657 }
@@ -540,8 +662,24 @@
540 662 public function register_basic_tags( $module ) {
541 663 $module->register( new TitleTag );
542 664 }
543 665
666 + /**
667 + * Handle editor mode for terms query args
668 + *
669 + * @param array $args Terms query arguments
670 + * @return array Modified arguments
671 + */
672 + public function handle_editor_mode_terms_args( $args ) {
673 + // Only modify if we're in Elementor editor mode
674 + if ( $this->elementor && $this->elementor->editor->is_edit_mode() ) {
675 + // Show empty categories in editor mode for better preview
676 + $args['hide_empty'] = false;
677 + }
678 +
679 + return $args;
680 + }
681 +
544 682 public function register_basic_widgets( $widgets_manager ) {
545 683 foreach ( $this->basic_widget_lists() as $value ) {
546 684 $widgets_manager->register( new $value );
547 685 }
@@ -555,12 +693,15 @@
555 693 * @since 1.3.0
556 694 */
557 695 private function basic_widget_lists() {
558 696 $widget_arr = [
559 - 'betterdocs-elementor-search-form' => SearchForm::class,
560 - 'betterdocs-elementor-category-grid' => CategoryGrid::class,
561 - 'betterdocs-elementor-category-box' => CategoryBox::class,
562 - 'betterdocs-faq-widget' => FAQ::class
697 + 'betterdocs-elementor-search-form' => SearchForm::class,
698 + 'betterdocs-elementor-category-grid' => CategoryGrid::class,
699 + 'betterdocs-elementor-category-box' => CategoryBox::class,
700 + 'betterdocs-elementor-category-slate' => CategorySlateLayout::class,
701 + 'betterdocs-faq-widget' => FAQ::class,
702 + 'betterdocs-code-snippet' => CodeSnippet::class,
703 + 'betterdocs-code-snippet-tab' => CodeSnippetTab::class
563 704 ];
564 705
565 706 return $widget_arr;
566 707 }
@@ -620,14 +761,32 @@
620 761 $assets->register( 'betterdocs-el-category-grid', 'elementor/css/category-grid.css' );
621 762 $assets->register( 'betterdocs-el-category-box', 'elementor/css/category-box.css' );
622 763 $assets->register( 'betterdocs-el-articles-list', 'elementor/css/articles-list.css' );
623 764 $assets->register( 'betterdocs-el-navigation', 'elementor/css/navigation.css' );
765 + $assets->register( 'betterdocs-code-snippet', 'public/css/code-snippet.css' );
766 + $assets->register( 'betterdocs-code-snippet-tab', 'public/css/code-snippet-tab.css' );
624 767
625 768 /**
626 769 * Widget's JS
627 770 */
628 771 $assets->register( 'betterdocs-el-category-grid', 'elementor/js/category-grid.js', ['jquery', 'betterdocs-category-toggler'] );
772 + $assets->register( 'betterdocs-code-snippet', 'public/js/code-snippet.js', ['jquery'] );
773 + $assets->register( 'betterdocs-code-snippet-tab', 'public/js/code-snippet-tab.js', ['jquery'] );
629 774
775 + /**
776 + * Localize This In Order To Know If This Block Is Arriving From Betterdocs Templates Or Not
777 + */
778 + betterdocs()->assets->localize( 'betterdocs-el-category-grid', 'betterdocsCategoryGridConfig', [
779 + 'is_betterdocs_templates' => betterdocs()->helper->is_templates() ? true : false,
780 + // Must also carry the lazy-load endpoint. wp_localize_script concatenates
781 + // every `var betterdocsCategoryGridConfig = …` for this object name and the
782 + // last assignment wins; shipping only is_betterdocs_templates here clobbered
783 + // Scripts.php's full config on Elementor pages, leaving ajax_url undefined so
784 + // loadLazyBody() bailed and sidebar lazy-load never fired.
785 + 'ajax_url' => admin_url( 'admin-ajax.php' ),
786 + 'lazy_load_action' => 'betterdocs_lazy_category_body',
787 + ] );
788 +
630 789 if ( betterdocs()->helper->is_el_templates() == true ) {
631 790 $assets->enqueue( 'betterdocs-elementor-editor', 'elementor/css/betterdocs-el-edit.css' );
632 791
633 792 if ( ! $this->is_pro_active ) {
@@ -653,19 +812,25 @@
653 812 * @since 1.3.0
654 813 */
655 814 public function builder_widget_list() {
656 815 return apply_filters( 'betterdocs_elementor_pro_widgets', [
657 - 'betterdocs-elementor-toc' => ToC::class,
658 - 'betterdocs-elementor-title' => Title::class,
659 - 'betterdocs-elementor-sidebar' => Sidebar::class,
660 - 'betterdocs-elementor-content' => Content::class,
661 - 'betterdocs-elementor-doc-date' => DocDate::class,
662 - 'betterdocs-elementor-doc-share' => DocShare::class,
663 - 'betterdocs-elementor-feedback' => Feedback::class,
664 - 'betterdocs-elementor-reactions' => Reactions::class,
665 - 'betterdocs-elementor-navigation' => Navigation::class,
666 - 'betterdocs-elementor-breadcrumbs' => Breadcrumbs::class,
667 - 'betterdocs-elementor-category-archive-list' => ArchiveList::class
816 + 'betterdocs-elementor-toc' => ToC::class,
817 + 'betterdocs-elementor-title' => Title::class,
818 + 'betterdocs-elementor-sidebar' => Sidebar::class,
819 + 'betterdocs-elementor-content' => Content::class,
820 + 'betterdocs-elementor-doc-date' => DocDate::class,
821 + 'betterdocs-elementor-doc-share' => DocShare::class,
822 + 'betterdocs-elementor-feedback' => Feedback::class,
823 + 'betterdocs-elementor-reactions' => Reactions::class,
824 + 'betterdocs-article-summary' => ArticleSummary::class,
825 + 'betterdocs-elementor-navigation' => Navigation::class,
826 + 'betterdocs-elementor-breadcrumbs' => Breadcrumbs::class,
827 + 'betterdocs-elementor-category-archive-list' => ArchiveList::class,
828 + 'betteredocs-elementor-reading-time' => ReadingTime::class,
829 + 'betterdocs-print-button' => PrintButton::class,
830 + 'betterdocs-elementor-doc-author' => DocAuthor::class,
831 + 'betteredocs-elementor-doc-category-archive-header' => CategoryArchiveHeader::class,
832 + 'betterdocs-elementor-tags' => Tags::class
668 833 ] );
669 834 }
670 835
671 836 /**
@@ -729,9 +894,9 @@
729 894 $categories = $this->template_categories();
730 895 if ( ! empty( $categories ) ) {
731 896 $library_data['types_data']['block']['categories'] = array_merge(
732 897 $categories,
733 - $library_data['types_data']['block']['categories']
898 + isset( $library_data['types_data']['block']['categories'] ) ? $library_data['types_data']['block']['categories'] : []
734 899 );
735 900 }
736 901 return $library_data;
737 902 }
@@ -772,17 +937,22 @@
772 937 * @return array|mixed
773 938 * @since 2.0.0
774 939 */
775 940 public function get_templates() {
776 - $_cache_key = 'betterdocs_template_library_' . betterdocs()->version;
777 - $templates = get_transient( $_cache_key );
941 + $_cache_key = 'betterdocs_template_library_' . betterdocs()->version;
942 + $source = new TemplateSource;
943 + $local_ids = array_column( $source->get_local_templates(), 'template_id' );
944 + $templates = get_transient( $_cache_key );
778 945
779 946 if ( ! $templates ) {
780 - $source = new TemplateSource;
781 - $templates = $source->get_items();
947 + // Only cache remote templates — local templates are always appended fresh below
948 + $all_templates = $source->get_items();
949 + $remote_only = array_values( array_filter( $all_templates, function ( $t ) use ( $local_ids ) {
950 + return ! in_array( $t['template_id'], $local_ids, true );
951 + } ) );
782 952
783 - if ( ! empty( $templates ) ) {
784 - $templates = array_map( function ( $template ) {
953 + if ( ! empty( $remote_only ) ) {
954 + $remote_only = array_map( function ( $template ) {
785 955 $template['id'] = $template['template_id'];
786 956 $template['tmpl_created'] = $template['date'];
787 957 $template['tags'] = json_encode( $template['tags'] );
788 958 $template['is_pro'] = $template['isPro'];
@@ -791,25 +961,52 @@
791 961 $template['trend_index'] = $template['trendIndex'];
792 962 $template['has_page_settings'] = $template['hasPageSettings'];
793 963
794 964 return $template;
795 - }, $templates );
965 + }, $remote_only );
796 966
797 - set_transient( $_cache_key, $templates, WEEK_IN_SECONDS );
967 + set_transient( $_cache_key, $remote_only, WEEK_IN_SECONDS );
968 + $templates = $remote_only;
798 969 } else {
799 970 $templates = [];
800 971 }
972 + } else {
973 + // Strip any local templates that may have been cached in a previous version
974 + $templates = array_values( array_filter( $templates, function ( $t ) use ( $local_ids ) {
975 + $id = isset( $t['template_id'] ) ? $t['template_id'] : ( isset( $t['id'] ) ? $t['id'] : '' );
976 + return ! in_array( $id, $local_ids, true );
977 + } ) );
801 978 }
802 979
803 - return $templates;
980 + // Always append local templates fresh so they are never stale-cached
981 + $local_templates = array_map( function ( $template ) {
982 + $template['id'] = $template['template_id'];
983 + $template['tmpl_created'] = $template['date'];
984 + $template['tags'] = json_encode( $template['tags'] );
985 + $template['is_pro'] = $template['isPro'];
986 + $template['access_level'] = $template['accessLevel'];
987 + $template['popularity_index'] = $template['popularityIndex'];
988 + $template['trend_index'] = $template['trendIndex'];
989 + $template['has_page_settings'] = $template['hasPageSettings'];
990 +
991 + return $template;
992 + }, $source->get_local_templates() );
993 +
994 + return array_merge( $templates, $local_templates );
804 995 }
805 996
806 997 public function modified_ajax_action( $ajax ) {
998 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Elementor's own ajax handler verifies nonce upstream.
807 999 if ( ! isset( $_REQUEST['actions'] ) ) {
808 1000 return;
809 1001 }
810 1002
811 - $actions = json_decode( stripslashes( $_REQUEST['actions'] ), true );
1003 + // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Elementor's ajax handler verifies nonce; raw JSON is structure-validated below.
1004 + $raw_actions = wp_unslash( $_REQUEST['actions'] );
1005 + $actions = json_decode( $raw_actions, true );
1006 + if ( ! is_array( $actions ) ) {
1007 + return;
1008 + }
812 1009 $data = false;
813 1010
814 1011 foreach ( $actions as $id => $action_data ) {
815 1012 if ( ! isset( $action_data['get_template_data'] ) ) {