PluginProbe
Elementor Website Builder – more than just a page builder / 3.34.0-beta1
Elementor Website Builder – more than just a page builder v3.34.0-beta1
4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 All 457 releases
elementor / includes / template-library / sources / local.php

local.php in Elementor Website Builder – more than just a page builder 3.34.0-beta1, at includes/template-library/sources/local.php

1,935 lines 53.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\TemplateLibrary;
3
4 use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
5 use Elementor\Core\Base\Document;
6 use Elementor\Core\Editor\Editor;
7 use Elementor\Core\Utils\Collection;
8 use Elementor\DB;
9 use Elementor\Core\Settings\Manager as SettingsManager;
10 use Elementor\Core\Settings\Page\Model;
11 use Elementor\Includes\TemplateLibrary\Sources\AdminMenuItems\Add_New_Template_Menu_Item;
12 use Elementor\Includes\TemplateLibrary\Sources\AdminMenuItems\Saved_Templates_Menu_Item;
13 use Elementor\Includes\TemplateLibrary\Sources\AdminMenuItems\Templates_Categories_Menu_Item;
14 use Elementor\Modules\Library\Documents\Library_Document;
15 use Elementor\Plugin;
16 use Elementor\Utils;
17 use Elementor\User;
18 use Elementor\Core\Isolation\Wordpress_Adapter;
19 use Elementor\Core\Isolation\Wordpress_Adapter_Interface;
20 use Elementor\Core\Isolation\Elementor_Adapter;
21 use Elementor\Core\Isolation\Elementor_Adapter_Interface;
22
23 if ( ! defined( 'ABSPATH' ) ) {
24 exit; // Exit if accessed directly.
25 }
26
27 /**
28 * Elementor template library local source.
29 *
30 * Elementor template library local source handler class is responsible for
31 * handling local Elementor templates saved by the user locally on his site.
32 *
33 * @since 1.0.0
34 */
35 class Source_Local extends Source_Base {
36
37 /**
38 * Elementor template-library post-type slug.
39 */
40 const CPT = 'elementor_library';
41
42 /**
43 * Elementor template-library taxonomy slug.
44 */
45 const TAXONOMY_TYPE_SLUG = 'elementor_library_type';
46
47 /**
48 * Elementor template-library category slug.
49 */
50 const TAXONOMY_CATEGORY_SLUG = 'elementor_library_category';
51
52 /**
53 * Elementor template-library meta key.
54 *
55 * @deprecated 2.3.0 Use `Elementor\Core\Base\Document::TYPE_META_KEY` const instead.
56 */
57 const TYPE_META_KEY = '_elementor_template_type';
58
59 /**
60 * Elementor template-library temporary files folder.
61 */
62 const TEMP_FILES_DIR = 'elementor/tmp';
63
64 /**
65 * Elementor template-library bulk export action name.
66 */
67 const BULK_EXPORT_ACTION = 'elementor_export_multiple_templates';
68
69 const ADMIN_MENU_SLUG = 'edit.php?post_type=elementor_library';
70
71 const ADMIN_MENU_PRIORITY = 10;
72
73 const ADMIN_SCREEN_ID = 'edit-elementor_library';
74
75 /**
76 * Template types.
77 *
78 * Holds the list of supported template types that can be displayed.
79 *
80 * @access private
81 * @static
82 *
83 * @var array
84 */
85 private static $template_types = [];
86
87 /**
88 * Post type object.
89 *
90 * Holds the post type object of the current post.
91 *
92 * @access private
93 *
94 * @var \WP_Post_Type
95 */
96 private $post_type_object;
97
98 /**
99 * @var Wordpress_Adapter_Interface
100 */
101 protected $wordpress_adapter = null;
102
103 /**
104 * @var Elementor_Adapter_Interface
105 */
106 protected $elementor_adapter = null;
107
108 /**
109 * @since 2.3.0
110 * @access public
111 * @static
112 * @return array
113 */
114 public static function get_template_types() {
115 return self::$template_types;
116 }
117
118 /**
119 * Get local template type.
120 *
121 * Retrieve the template type from the post meta.
122 *
123 * @since 1.0.0
124 * @access public
125 * @static
126 *
127 * @param int $template_id The template ID.
128 *
129 * @return mixed The value of meta data field.
130 */
131 public static function get_template_type( $template_id ) {
132 return get_post_meta( $template_id, Document::TYPE_META_KEY, true );
133 }
134
135 /**
136 * Is base templates screen.
137 *
138 * Whether the current screen base is edit and the post type is template.
139 *
140 * @since 1.0.0
141 * @access public
142 * @static
143 *
144 * @return bool True on base templates screen, False otherwise.
145 */
146 public static function is_base_templates_screen() {
147 global $current_screen;
148
149 if ( ! $current_screen ) {
150 return false;
151 }
152
153 return 'edit' === $current_screen->base && self::CPT === $current_screen->post_type;
154 }
155
156 /**
157 * Add template type.
158 *
159 * Register new template type to the list of supported local template types.
160 *
161 * @since 1.0.3
162 * @access public
163 * @static
164 *
165 * @param string $type Template type.
166 */
167 public static function add_template_type( $type ) {
168 self::$template_types[ $type ] = $type;
169 }
170
171 /**
172 * Remove template type.
173 *
174 * Remove existing template type from the list of supported local template
175 * types.
176 *
177 * @since 1.8.0
178 * @access public
179 * @static
180 *
181 * @param string $type Template type.
182 */
183 public static function remove_template_type( $type ) {
184 if ( isset( self::$template_types[ $type ] ) ) {
185 unset( self::$template_types[ $type ] );
186 }
187 }
188
189 public static function get_admin_url( $relative = false ) {
190 $base_url = self::ADMIN_MENU_SLUG;
191 if ( ! $relative ) {
192 $base_url = admin_url( $base_url );
193 }
194
195 return add_query_arg( 'tabs_group', 'library', $base_url );
196 }
197
198 /**
199 * Get local template ID.
200 *
201 * Retrieve the local template ID.
202 *
203 * @since 1.0.0
204 * @access public
205 *
206 * @return string The local template ID.
207 */
208 public function get_id() {
209 return 'local';
210 }
211
212 /**
213 * Get local template title.
214 *
215 * Retrieve the local template title.
216 *
217 * @since 1.0.0
218 * @access public
219 *
220 * @return string The local template title.
221 */
222 public function get_title() {
223 return esc_html__( 'Local', 'elementor' );
224 }
225
226 /**
227 * Register local template data.
228 *
229 * Used to register custom template data like a post type, a taxonomy or any
230 * other data.
231 *
232 * The local template class registers a new `elementor_library` post type
233 * and an `elementor_library_type` taxonomy. They are used to store data for
234 * local templates saved by the user on his site.
235 *
236 * @since 1.0.0
237 * @access public
238 */
239 public function register_data() {
240 $labels = [
241 'name' => esc_html_x( 'My Templates', 'Template Library', 'elementor' ),
242 'singular_name' => esc_html_x( 'Template', 'Template Library', 'elementor' ),
243 'add_new' => esc_html__( 'Add New Template', 'elementor' ),
244 'add_new_item' => esc_html__( 'Add New Template', 'elementor' ),
245 'edit_item' => esc_html__( 'Edit Template', 'elementor' ),
246 'new_item' => esc_html__( 'New Template', 'elementor' ),
247 'all_items' => esc_html__( 'All Templates', 'elementor' ),
248 'view_item' => esc_html__( 'View Template', 'elementor' ),
249 'search_items' => esc_html__( 'Search Template', 'elementor' ),
250 'not_found' => esc_html__( 'No Templates found', 'elementor' ),
251 'not_found_in_trash' => esc_html__( 'No Templates found in Trash', 'elementor' ),
252 'parent_item_colon' => esc_html__( 'Parent Template:', 'elementor' ),
253 'menu_name' => esc_html_x( 'Templates', 'Template Library', 'elementor' ),
254 ];
255
256 $args = [
257 'labels' => $labels,
258 'public' => true,
259 'rewrite' => false,
260 'menu_icon' => 'dashicons-admin-page',
261 'show_ui' => true,
262 'show_in_menu' => true,
263 'show_in_nav_menus' => false,
264 'exclude_from_search' => true,
265 'capability_type' => 'post',
266 'hierarchical' => false,
267 'supports' => [ 'title', 'thumbnail', 'author', 'elementor', 'custom-fields' ],
268 'show_in_rest' => true,
269 ];
270
271 $this->avoid_rest_access_for_non_admins();
272
273 /**
274 * Register template library post type args.
275 *
276 * Filters the post type arguments when registering elementor template library post type.
277 *
278 * @since 1.0.0
279 *
280 * @param array $args Arguments for registering a post type.
281 */
282 $args = apply_filters( 'elementor/template_library/sources/local/register_post_type_args', $args );
283
284 $this->post_type_object = register_post_type( self::CPT, $args );
285
286 $args = [
287 'hierarchical' => false,
288 'show_ui' => false,
289 'show_in_nav_menus' => false,
290 'show_admin_column' => true,
291 'query_var' => is_admin(),
292 'rewrite' => false,
293 'public' => false,
294 'label' => esc_html_x( 'Type', 'Template Library', 'elementor' ),
295 ];
296
297 /**
298 * Register template library taxonomy args.
299 *
300 * Filters the taxonomy arguments when registering elementor template library taxonomy.
301 *
302 * @since 1.0.0
303 *
304 * @param array $args Arguments for registering a taxonomy.
305 */
306 $args = apply_filters( 'elementor/template_library/sources/local/register_taxonomy_args', $args );
307
308 $cpts_to_associate = [ self::CPT ];
309
310 /**
311 * Custom post types to associate.
312 *
313 * Filters the list of custom post types when registering elementor template library taxonomy.
314 *
315 * @since 1.0.0
316 *
317 * @param array $cpts_to_associate Custom post types. Default is `elementor_library` post type.
318 */
319 $cpts_to_associate = apply_filters( 'elementor/template_library/sources/local/register_taxonomy_cpts', $cpts_to_associate );
320
321 register_taxonomy( self::TAXONOMY_TYPE_SLUG, $cpts_to_associate, $args );
322
323 /**
324 * Categories
325 */
326 $args = [
327 'hierarchical' => true,
328 'show_ui' => true,
329 'show_in_nav_menus' => false,
330 'show_admin_column' => true,
331 'query_var' => is_admin(),
332 'rewrite' => false,
333 'public' => false,
334 'labels' => [
335 'name' => esc_html_x( 'Categories', 'Template Library', 'elementor' ),
336 'singular_name' => esc_html_x( 'Category', 'Template Library', 'elementor' ),
337 'all_items' => esc_html_x( 'All Categories', 'Template Library', 'elementor' ),
338 ],
339 ];
340
341 /**
342 * Register template library category args.
343 *
344 * Filters the category arguments when registering elementor template library category.
345 *
346 * @since 2.4.0
347 *
348 * @param array $args Arguments for registering a category.
349 */
350 $args = apply_filters( 'elementor/template_library/sources/local/register_category_args', $args );
351
352 register_taxonomy( self::TAXONOMY_CATEGORY_SLUG, self::CPT, $args );
353 }
354
355 /**
356 * Remove Add New item from admin menu.
357 *
358 * Fired by `admin_menu` action.
359 *
360 * @since 2.4.0
361 * @access public
362 */
363 private function admin_menu_reorder( Admin_Menu_Manager $admin_menu ) {
364 global $submenu;
365
366 if ( ! isset( $submenu[ static::ADMIN_MENU_SLUG ] ) ) {
367 return;
368 }
369
370 remove_submenu_page( static::ADMIN_MENU_SLUG, static::ADMIN_MENU_SLUG );
371
372 $add_new_slug = 'post-new.php?post_type=' . static::CPT;
373 $category_slug = 'edit-tags.php?taxonomy=' . static::TAXONOMY_CATEGORY_SLUG . '&amp;post_type=' . static::CPT;
374
375 $library_submenu = new Collection( $submenu[ static::ADMIN_MENU_SLUG ] );
376
377 $add_new_item = $library_submenu->find( function ( $item ) use ( $add_new_slug ) {
378 return $add_new_slug === $item[2];
379 } );
380
381 $categories_item = $library_submenu->find( function ( $item ) use ( $category_slug ) {
382 return $category_slug === $item[2];
383 } );
384
385 if ( $add_new_item ) {
386 remove_submenu_page( static::ADMIN_MENU_SLUG, $add_new_slug );
387
388 $admin_menu->register( admin_url( static::ADMIN_MENU_SLUG . '#add_new' ), new Add_New_Template_Menu_Item() );
389 }
390
391 if ( $categories_item ) {
392 remove_submenu_page( static::ADMIN_MENU_SLUG, $category_slug );
393
394 $admin_menu->register( $category_slug, new Templates_Categories_Menu_Item() );
395 }
396 }
397
398 /**
399 * Add a `current` CSS class to the `Saved Templates` submenu page when it's active.
400 * It should work by default, but since we interfere with WordPress' builtin CPT menus it doesn't work properly.
401 *
402 * @return void
403 */
404 private function admin_menu_set_current() {
405 global $submenu;
406
407 if ( $this->is_current_screen() ) {
408 $library_submenu = &$submenu[ static::ADMIN_MENU_SLUG ];
409 $library_title = $this->get_library_title();
410
411 foreach ( $library_submenu as &$item ) {
412 if ( $library_title === $item[0] ) {
413 if ( ! isset( $item[4] ) ) {
414 $item[4] = '';
415 }
416 $item[4] .= ' current';
417 }
418 }
419 }
420 }
421
422 private function register_admin_menu( Admin_Menu_Manager $admin_menu ) {
423 $admin_menu->register( static::get_admin_url( true ), new Saved_Templates_Menu_Item() );
424 }
425
426 public function admin_title( $admin_title, $title ) {
427 $library_title = $this->get_library_title();
428
429 if ( $library_title ) {
430 $admin_title = str_replace( $title, $library_title, $admin_title );
431 }
432
433 return $admin_title;
434 }
435
436 public function replace_admin_heading() {
437 $library_title = $this->get_library_title();
438
439 if ( $library_title ) {
440 global $post_type_object;
441
442 $post_type_object->labels->name = $library_title;
443 }
444 }
445
446 /**
447 * Get local templates.
448 *
449 * Retrieve local templates saved by the user on his site.
450 *
451 * @since 1.0.0
452 * @access public
453 *
454 * @param array $args Optional. Filter templates based on a set of
455 * arguments. Default is an empty array.
456 *
457 * @return array Local templates.
458 */
459 public function get_items( $args = [] ) {
460 $template_types = array_values( self::$template_types );
461
462 if ( ! empty( $args['type'] ) ) {
463 $template_types = $args['type'];
464 unset( $args['type'] );
465 }
466
467 $defaults = [
468 'post_type' => self::CPT,
469 'post_status' => 'publish',
470 'posts_per_page' => -1,
471 'orderby' => 'title',
472 'order' => 'ASC',
473 'meta_query' => [
474 [
475 'key' => Document::TYPE_META_KEY,
476 'value' => $template_types,
477 ],
478 ],
479 ];
480
481 $query_args = wp_parse_args( $args, $defaults );
482
483 $templates_query = new \WP_Query( $query_args );
484
485 $templates = [];
486
487 if ( $templates_query->have_posts() ) {
488 foreach ( $templates_query->get_posts() as $post ) {
489 $templates[] = $this->get_item( $post->ID );
490 }
491 }
492
493 return $templates;
494 }
495
496 /**
497 * Save local template.
498 *
499 * Save new or update existing template on the database.
500 *
501 * @since 1.0.0
502 * @access public
503 *
504 * @param array $template_data Local template data.
505 *
506 * @return \WP_Error|int The ID of the saved/updated template, `WP_Error` otherwise.
507 */
508 public function save_item( $template_data ) {
509 if ( ! current_user_can( $this->post_type_object->cap->edit_posts ) ) {
510 return new \WP_Error( 'save_error', esc_html__( 'Access denied.', 'elementor' ) );
511 }
512
513 $defaults = [
514 'title' => esc_html__( '(no title)', 'elementor' ),
515 'page_settings' => [],
516 ];
517
518 $template_data = wp_parse_args( $template_data, $defaults );
519 $template_data['status'] = current_user_can( 'publish_posts' ) ? 'publish' : 'pending';
520
521 // BC: Allow importing any template type when using CLI
522 // to support users that rely on this mechanism.
523 $should_check_template_type = ! $this->is_wp_cli();
524
525 if (
526 $should_check_template_type &&
527 ! $this->is_valid_template_type( $template_data['type'] )
528 ) {
529 return new \WP_Error( 'invalid_template_type', esc_html__( 'Invalid template type.', 'elementor' ) );
530 }
531
532 $document = Plugin::$instance->documents->create(
533 $template_data['type'],
534 [
535 'post_title' => $template_data['title'],
536 'post_status' => $template_data['status'],
537 ]
538 );
539
540 if ( is_wp_error( $document ) ) {
541 /**
542 * @var \WP_Error $document
543 */
544 return $document;
545 }
546
547 if ( ! empty( $template_data['content'] ) ) {
548 $template_data['content'] = $this->replace_elements_ids( $template_data['content'] );
549 }
550
551 $document->save( [
552 'elements' => $template_data['content'],
553 'settings' => $template_data['page_settings'],
554 ] );
555
556 $template_id = $document->get_main_id();
557
558 /**
559 * After template library save.
560 *
561 * Fires after Elementor template library was saved.
562 *
563 * @since 1.0.1
564 *
565 * @param int $template_id The ID of the template.
566 * @param array $template_data The template data.
567 */
568 do_action( 'elementor/template-library/after_save_template', $template_id, $template_data );
569
570 /**
571 * After template library update.
572 *
573 * Fires after Elementor template library was updated.
574 *
575 * @since 1.0.1
576 *
577 * @param int $template_id The ID of the template.
578 * @param array $template_data The template data.
579 */
580 do_action( 'elementor/template-library/after_update_template', $template_id, $template_data );
581
582 return $template_id;
583 }
584
585 protected function is_valid_template_type( $type ) {
586 $document_class = Plugin::$instance->documents->get_document_type( $type, false );
587
588 if ( ! $document_class ) {
589 return false;
590 }
591
592 $cpt = $document_class::get_property( 'cpt' );
593
594 if ( ! $cpt || ! is_array( $cpt ) || 1 !== count( $cpt ) ) {
595 return false;
596 }
597
598 $is_valid_template_type = in_array( static::CPT, $cpt, true );
599
600 return apply_filters(
601 'elementor/template_library/sources/local/is_valid_template_type',
602 $is_valid_template_type,
603 $cpt,
604 );
605 }
606
607 /** For testing purposes only, in order to be able to mock the `WP_CLI` constant. */
608 protected function is_wp_cli() {
609 return Utils::is_wp_cli();
610 }
611
612 /**
613 * Update local template.
614 *
615 * Update template on the database.
616 *
617 * @since 1.0.0
618 * @access public
619 *
620 * @param array $new_data New template data.
621 *
622 * @return \WP_Error|true True if template updated, `WP_Error` otherwise.
623 */
624 public function update_item( $new_data ) {
625 if ( ! current_user_can( $this->post_type_object->cap->edit_post, $new_data['id'] ) ) {
626 return new \WP_Error( 'save_error', esc_html__( 'Access denied.', 'elementor' ) );
627 }
628
629 $document = Plugin::$instance->documents->get( $new_data['id'] );
630
631 if ( ! $document ) {
632 return new \WP_Error( 'save_error', esc_html__( 'Template not exist.', 'elementor' ) );
633 }
634
635 $save_data = [];
636
637 if ( isset( $new_data['title'] ) ) {
638 $save_data['post_title'] = $new_data['title'];
639 }
640
641 if ( isset( $new_data['content'] ) ) {
642 $save_data['elements'] = $new_data['content'];
643 }
644
645 $document->save( $save_data );
646
647 /**
648 * After template library update.
649 *
650 * Fires after Elementor template library was updated.
651 *
652 * @since 1.0.0
653 *
654 * @param int $new_data_id The ID of the new template.
655 * @param array $new_data The new template data.
656 */
657 do_action( 'elementor/template-library/after_update_template', $new_data['id'], $new_data );
658
659 return true;
660 }
661
662 /**
663 * Get local template.
664 *
665 * Retrieve a single local template saved by the user on his site.
666 *
667 * @since 1.0.0
668 * @access public
669 *
670 * @param int $template_id The template ID.
671 *
672 * @return array Local template.
673 */
674 public function get_item( $template_id ) {
675 $post = get_post( $template_id );
676
677 $user = get_user_by( 'id', $post->post_author );
678
679 $page = SettingsManager::get_settings_managers( 'page' )->get_model( $template_id );
680
681 $page_settings = $page->get_data( 'settings' );
682
683 $date = strtotime( $post->post_date );
684
685 $data = [
686 'template_id' => $post->ID,
687 'source' => $this->get_id(),
688 'type' => self::get_template_type( $post->ID ),
689 'title' => $post->post_title,
690 'thumbnail' => get_the_post_thumbnail_url( $post ),
691 'date' => $date,
692 'human_date' => date_i18n( get_option( 'date_format' ), $date ),
693 'human_modified_date' => date_i18n( get_option( 'date_format' ), strtotime( $post->post_modified ) ),
694 'author' => $user->display_name,
695 'status' => $post->post_status,
696 'hasPageSettings' => ! empty( $page_settings ),
697 'tags' => [],
698 'export_link' => $this->get_export_link( $template_id ),
699 'url' => get_permalink( $post->ID ),
700 ];
701
702 /**
703 * Get template library template.
704 *
705 * Filters the template data when retrieving a single template from the
706 * template library.
707 *
708 * @since 1.0.0
709 *
710 * @param array $data Template data.
711 */
712 $data = apply_filters( 'elementor/template-library/get_template', $data );
713
714 return $data;
715 }
716
717 /**
718 * Get template data.
719 *
720 * Retrieve the data of a single local template saved by the user on his site.
721 *
722 * @since 1.5.0
723 * @access public
724 *
725 * @param array $args Custom template arguments.
726 *
727 * @return array Local template data.
728 */
729 public function get_data( array $args ) {
730 $template_id = $args['template_id'];
731
732 $document = Plugin::$instance->documents->get( $template_id );
733 $content = [];
734
735 if ( $document ) {
736 // TODO: Validate the data (in JS too!).
737 if ( ! empty( $args['display'] ) ) {
738 $content = $document->get_elements_raw_data( null, true );
739 } else {
740 $content = $document->get_elements_data();
741 }
742
743 if ( ! empty( $content ) ) {
744 $content = $this->replace_elements_ids( $content );
745
746 /**
747 * Filters the template content data.
748 *
749 * This filter allows third-party plugins to modify template content
750 * when loaded via get_template_data(), making the behavior consistent
751 * with Frontend::get_builder_content().
752 *
753 * @since 3.34.0
754 *
755 * @param array $content The template content data.
756 * @param int $template_id The template ID.
757 */
758 $content = apply_filters( 'elementor/frontend/builder_content_data', $content, $template_id );
759 }
760 }
761
762 $data = [
763 'content' => $content,
764 ];
765
766 if ( ! empty( $args['with_page_settings'] ) ) {
767 $page = SettingsManager::get_settings_managers( 'page' )->get_model( $args['template_id'] );
768
769 $data['page_settings'] = $page->get_data( 'settings' );
770 }
771
772 return $data;
773 }
774
775 /**
776 * Delete local template.
777 *
778 * Delete template from the database.
779 *
780 * @since 1.0.0
781 * @access public
782 *
783 * @param int $template_id The template ID.
784 *
785 * @return \WP_Post|\WP_Error|false|null Post data on success, false or null
786 * or 'WP_Error' on failure.
787 */
788 public function delete_template( $template_id ) {
789 if ( ! current_user_can( $this->post_type_object->cap->delete_post, $template_id ) ) {
790 return new \WP_Error( 'template_error', esc_html__( 'Access denied.', 'elementor' ) );
791 }
792
793 return wp_delete_post( $template_id, true );
794 }
795
796 public function bulk_delete_items( array $template_ids ) {
797 foreach ( $template_ids as $template_id ) {
798 if ( ! current_user_can( $this->post_type_object->cap->delete_post, $template_id ) ) {
799 return new \WP_Error( 'template_error', esc_html__( 'Access denied.', 'elementor' ) );
800 }
801 }
802
803 foreach ( $template_ids as $template_id ) {
804 wp_delete_post( $template_id, true );
805 }
806
807 return true;
808 }
809
810 /**
811 * Export local template.
812 *
813 * Export template to a file.
814 *
815 * @since 1.0.0
816 * @access public
817 *
818 * @param int $template_id The template ID.
819 *
820 * @return \WP_Error WordPress error if template export failed.
821 */
822 public function export_template( $template_id ) {
823 $permissions_error = $this->validate_template_export_permissions( $template_id );
824
825 if ( is_wp_error( $permissions_error ) ) {
826 return $permissions_error;
827 }
828
829 $file_data = $this->prepare_template_export( $template_id );
830
831 if ( is_wp_error( $file_data ) ) {
832 return $file_data;
833 }
834
835 $this->send_file_headers( $file_data['name'], strlen( $file_data['content'] ) );
836
837 $this->serve_file( $file_data['content'] );
838
839 die;
840 }
841
842 private function validate_template_export_permissions( $template_id ) {
843 $post_id = intval( $template_id );
844 if ( get_post_type( $post_id ) !== self::CPT ) {
845 return new \WP_Error( 'template_error', esc_html__( 'Invalid template type or template does not exist.', 'elementor' ) );
846 }
847
848 $post_status = get_post_status( $post_id );
849 if ( 'private' === $post_status && ! current_user_can( 'read_private_posts', $post_id ) ) {
850 return new \WP_Error( 'template_error', esc_html__( 'You do not have permission to access this template.', 'elementor' ) );
851 }
852
853 if ( 'publish' !== $post_status && ! current_user_can( 'edit_post', $post_id ) ) {
854 return new \WP_Error( 'template_error', esc_html__( 'You do not have permission to export this template.', 'elementor' ) );
855 }
856
857 return null;
858 }
859
860 /**
861 * Export multiple local templates.
862 *
863 * Export multiple template to a ZIP file.
864 *
865 * @since 1.6.0
866 * @access public
867 *
868 * @param array $template_ids An array of template IDs.
869 *
870 * @return \WP_Error WordPress error if export failed.
871 */
872 public function export_multiple_templates( array $template_ids ) {
873 $files = [];
874
875 $wp_upload_dir = wp_upload_dir();
876
877 $temp_path = $wp_upload_dir['basedir'] . '/' . self::TEMP_FILES_DIR;
878
879 // Create temp path if it doesn't exist
880 wp_mkdir_p( $temp_path );
881
882 // Create all json files
883 foreach ( $template_ids as $template_id ) {
884 $file_data = $this->prepare_template_export( $template_id );
885
886 if ( is_wp_error( $file_data ) ) {
887 continue;
888 }
889
890 $complete_path = $temp_path . '/' . $file_data['name'];
891
892 $put_contents = file_put_contents( $complete_path, $file_data['content'] );
893
894 if ( ! $put_contents ) {
895 return new \WP_Error( '404', sprintf( 'Cannot create file "%s".', $file_data['name'] ) );
896 }
897
898 $files[] = [
899 'path' => $complete_path,
900 'name' => $file_data['name'],
901 ];
902 }
903
904 if ( ! $files ) {
905 return new \WP_Error( 'empty_files', 'There is no files to export (probably all the requested templates are empty).' );
906 }
907
908 // Create temporary .zip file
909 $zip_archive_filename = 'elementor-templates-' . gmdate( 'Y-m-d' ) . '.zip';
910
911 $zip_archive = new \ZipArchive();
912
913 $zip_complete_path = $temp_path . '/' . $zip_archive_filename;
914
915 $zip_archive->open( $zip_complete_path, \ZipArchive::CREATE );
916
917 foreach ( $files as $file ) {
918 $zip_archive->addFile( $file['path'], $file['name'] );
919 }
920
921 $zip_archive->close();
922
923 foreach ( $files as $file ) {
924 unlink( $file['path'] );
925 }
926
927 $this->send_file_headers( $zip_archive_filename, $this->filesize( $zip_complete_path ) );
928
929 $this->serve_zip( $zip_complete_path );
930
931 unlink( $zip_complete_path );
932
933 die;
934 }
935
936 /**
937 * Import local template.
938 *
939 * Import template from a file.
940 *
941 * @since 1.0.0
942 * @access public
943 *
944 * @param string $name - The file name.
945 * @param string $path - The file path.
946 * @return \WP_Error|array An array of items on success, 'WP_Error' on failure.
947 */
948 public function import_template( $name, $path ) {
949 if ( empty( $path ) ) {
950 return new \WP_Error( 'file_error', 'Please upload a file to import' );
951 }
952
953 // Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
954 Plugin::$instance->uploads_manager->set_elementor_upload_state( true );
955
956 $items = [];
957
958 // If the import file is a Zip file with potentially multiple JSON files
959 if ( 'zip' === pathinfo( $name, PATHINFO_EXTENSION ) ) {
960 $extracted_files = Plugin::$instance->uploads_manager->extract_and_validate_zip( $path, [ 'json' ] );
961
962 if ( is_wp_error( $extracted_files ) ) {
963 // Delete the temporary extraction directory, since it's now not necessary.
964 Plugin::$instance->uploads_manager->remove_file_or_dir( $extracted_files['extraction_directory'] );
965
966 return $extracted_files;
967 }
968
969 foreach ( $extracted_files['files'] as $file_path ) {
970 // Skip macOS metadata files and folders
971 if ( false !== strpos( $file_path, '__MACOSX' ) || '.' === basename( $file_path )[0] ) {
972 continue;
973 }
974
975 $import_result = $this->import_single_template( $file_path );
976
977 if ( is_wp_error( $import_result ) ) {
978 // Skip failed files
979 continue;
980 }
981
982 $items[] = $import_result;
983 }
984
985 // Delete the temporary extraction directory, since it's now not necessary.
986 Plugin::$instance->uploads_manager->remove_file_or_dir( $extracted_files['extraction_directory'] );
987 } else {
988 // If the import file is a single JSON file
989 $import_result = $this->import_single_template( $path );
990
991 if ( is_wp_error( $import_result ) ) {
992 return $import_result;
993 }
994
995 $items[] = $import_result;
996 }
997
998 return $items;
999 }
1000
1001 /**
1002 * Post row actions.
1003 *
1004 * Add an export link to the template library action links table list.
1005 *
1006 * Fired by `post_row_actions` filter.
1007 *
1008 * @since 1.0.0
1009 * @access public
1010 *
1011 * @param array $actions An array of row action links.
1012 * @param \WP_Post $post The post object.
1013 *
1014 * @return array An updated array of row action links.
1015 */
1016 public function post_row_actions( $actions, \WP_Post $post ) {
1017 if ( self::is_base_templates_screen() ) {
1018 if ( $this->is_template_supports_export( $post->ID ) ) {
1019 $actions['export-template'] = sprintf( '<a href="%1$s">%2$s</a>', $this->get_export_link( $post->ID ), esc_html__( 'Export Template', 'elementor' ) );
1020 }
1021 }
1022
1023 return $actions;
1024 }
1025
1026 /**
1027 * Admin import template form.
1028 *
1029 * The import form displayed in "My Library" screen in WordPress dashboard.
1030 *
1031 * The form allows the user to import template in json/zip format to the site.
1032 *
1033 * Fired by `admin_footer` action.
1034 *
1035 * @since 1.0.0
1036 * @access public
1037 */
1038 public function admin_import_template_form() {
1039 if ( ! self::is_base_templates_screen() || ! User::is_current_user_can_upload_json() ) {
1040 return;
1041 }
1042
1043 /** @var \Elementor\Core\Common\Modules\Ajax\Module $ajax */
1044 $ajax = Plugin::$instance->common->get_component( 'ajax' );
1045 ?>
1046 <div id="elementor-hidden-area">
1047 <a id="elementor-import-template-trigger" class="page-title-action"><?php echo esc_html__( 'Import Templates', 'elementor' ); ?></a>
1048 <div id="elementor-import-template-area">
1049 <div id="elementor-import-template-title"><?php echo esc_html__( 'Choose an Elementor template JSON file or a .zip archive of Elementor templates, and add them to the list of templates available in your library.', 'elementor' ); ?></div>
1050 <form id="elementor-import-template-form" method="post" action="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" enctype="multipart/form-data">
1051 <input type="hidden" name="action" value="elementor_library_direct_actions">
1052 <input type="hidden" name="library_action" value="direct_import_template">
1053 <input type="hidden" name="_nonce" value="<?php Utils::print_unescaped_internal_string( $ajax->create_nonce() ); ?>">
1054 <fieldset id="elementor-import-template-form-inputs">
1055 <input type="file" name="file" accept=".json,application/json,.zip,application/octet-stream,application/zip,application/x-zip,application/x-zip-compressed" required>
1056 <input id="e-import-template-action" type="submit" class="button" value="<?php echo esc_attr__( 'Import Now', 'elementor' ); ?>">
1057 </fieldset>
1058 </form>
1059 </div>
1060 </div>
1061 <?php
1062 }
1063
1064 /**
1065 * Block template frontend
1066 *
1067 * Don't display the single view of the template library post type in the
1068 * frontend, for users that don't have the proper permissions.
1069 *
1070 * Fired by `template_redirect` action.
1071 *
1072 * @since 1.0.0
1073 * @access public
1074 */
1075 public function block_template_frontend() {
1076 if ( is_singular( self::CPT ) && ! current_user_can( Editor::EDITING_CAPABILITY ) ) {
1077 wp_safe_redirect( site_url(), 301 );
1078 die;
1079 }
1080 }
1081
1082 /**
1083 * Is template library supports export.
1084 *
1085 * Whether the template library supports export.
1086 *
1087 * Template saved by the user locally on his site, support export by default
1088 * but this can be changed using a filter.
1089 *
1090 * @since 1.0.0
1091 * @access public
1092 *
1093 * @param int $template_id The template ID.
1094 *
1095 * @return bool Whether the template library supports export.
1096 */
1097 public function is_template_supports_export( $template_id ) {
1098 $export_support = true;
1099
1100 /**
1101 * Is template library supports export.
1102 *
1103 * Filters whether the template library supports export.
1104 *
1105 * @since 1.0.0
1106 *
1107 * @param bool $export_support Whether the template library supports export.
1108 * Default is true.
1109 * @param int $template_id Post ID.
1110 */
1111 $export_support = apply_filters( 'elementor/template_library/is_template_supports_export', $export_support, $template_id );
1112
1113 return $export_support;
1114 }
1115
1116 /**
1117 * Remove Elementor post state.
1118 *
1119 * Remove the 'elementor' post state from the display states of the post.
1120 *
1121 * Used to remove the 'elementor' post state from the template library items.
1122 *
1123 * Fired by `display_post_states` filter.
1124 *
1125 * @since 1.8.0
1126 * @access public
1127 *
1128 * @param array $post_states An array of post display states.
1129 * @param \WP_Post $post The current post object.
1130 *
1131 * @return array Updated array of post display states.
1132 */
1133 public function remove_elementor_post_state_from_library( $post_states, $post ) {
1134 if ( self::CPT === $post->post_type && isset( $post_states['elementor'] ) ) {
1135 unset( $post_states['elementor'] );
1136 }
1137 return $post_states;
1138 }
1139
1140 /**
1141 * Get template export link.
1142 *
1143 * Retrieve the link used to export a single template based on the template
1144 * ID.
1145 *
1146 * @since 2.0.0
1147 * @access private
1148 *
1149 * @param int $template_id The template ID.
1150 *
1151 * @return string Template export URL.
1152 */
1153 private function get_export_link( $template_id ) {
1154 // TODO: BC since 2.3.0 - Use `$ajax->create_nonce()`
1155 /** @var \Elementor\Core\Common\Modules\Ajax\Module $ajax */
1156 // $ajax = Plugin::$instance->common->get_component( 'ajax' );
1157
1158 return add_query_arg(
1159 [
1160 'action' => 'elementor_library_direct_actions',
1161 'library_action' => 'export_template',
1162 'source' => $this->get_id(),
1163 '_nonce' => wp_create_nonce( 'elementor_ajax' ),
1164 'template_id' => $template_id,
1165 ],
1166 admin_url( 'admin-ajax.php' )
1167 );
1168 }
1169
1170 /**
1171 * On template save.
1172 *
1173 * Run this method when template is being saved.
1174 *
1175 * Fired by `save_post` action.
1176 *
1177 * @since 1.0.1
1178 * @access public
1179 *
1180 * @param int $post_id Post ID.
1181 * @param \WP_Post $post The current post object.
1182 */
1183 public function on_save_post( $post_id, \WP_Post $post ) {
1184 if ( self::CPT !== $post->post_type ) {
1185 return;
1186 }
1187
1188 if ( self::get_template_type( $post_id ) ) { // It's already with a type
1189 return;
1190 }
1191
1192 // Don't save type on import, the importer will do it.
1193 if ( did_action( 'import_start' ) ) {
1194 return;
1195 }
1196
1197 $this->save_item_type( $post_id, 'page' );
1198 }
1199
1200 /**
1201 * Save item type.
1202 *
1203 * When saving/updating templates, this method is used to update the post
1204 * meta data and the taxonomy.
1205 *
1206 * @since 1.0.1
1207 * @access private
1208 *
1209 * @param int $post_id Post ID.
1210 * @param string $type Item type.
1211 */
1212 private function save_item_type( $post_id, $type ) {
1213 update_post_meta( $post_id, Document::TYPE_META_KEY, $type );
1214
1215 wp_set_object_terms( $post_id, $type, self::TAXONOMY_TYPE_SLUG );
1216 }
1217
1218 /**
1219 * Bulk export action.
1220 *
1221 * Adds an 'Export' action to the Bulk Actions drop-down in the template
1222 * library.
1223 *
1224 * Fired by `bulk_actions-edit-elementor_library` filter.
1225 *
1226 * @since 1.6.0
1227 * @access public
1228 *
1229 * @param array $actions An array of the available bulk actions.
1230 *
1231 * @return array An array of the available bulk actions.
1232 */
1233 public function admin_add_bulk_export_action( $actions ) {
1234 $actions[ self::BULK_EXPORT_ACTION ] = esc_html__( 'Export', 'elementor' );
1235
1236 return $actions;
1237 }
1238
1239 /**
1240 * Add bulk export action.
1241 *
1242 * Handles the template library bulk export action.
1243 *
1244 * Fired by `handle_bulk_actions-edit-elementor_library` filter.
1245 *
1246 * @since 1.6.0
1247 * @access public
1248 *
1249 * @param string $redirect_to The redirect URL.
1250 * @param string $action The action being taken.
1251 * @param array $post_ids The items to take the action on.
1252 */
1253 public function admin_export_multiple_templates( $redirect_to, $action, $post_ids ) {
1254 if ( self::BULK_EXPORT_ACTION === $action ) {
1255 $result = $this->export_multiple_templates( $post_ids );
1256
1257 // If you reach this line, the export failed
1258 // PHPCS - Not user input.
1259 wp_die( $result->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1260 }
1261 }
1262
1263 /**
1264 * Print admin tabs.
1265 *
1266 * Used to output the template library tabs with their labels.
1267 *
1268 * Fired by `views_edit-elementor_library` filter.
1269 *
1270 * @since 2.0.0
1271 * @access public
1272 *
1273 * @param array $views An array of available list table views.
1274 *
1275 * @return array An updated array of available list table views.
1276 */
1277 public function admin_print_tabs( $views ) {
1278 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required to retrieve the value.
1279 $current_type = Utils::get_super_global_value( $_REQUEST, self::TAXONOMY_TYPE_SLUG ) ?? '';
1280 $active_class = $current_type ? '' : ' nav-tab-active';
1281 $current_tabs_group = $this->get_current_tab_group();
1282
1283 $url_args = [
1284 'post_type' => self::CPT,
1285 'tabs_group' => $current_tabs_group,
1286 ];
1287
1288 $baseurl = add_query_arg( $url_args, admin_url( 'edit.php' ) );
1289
1290 $filter = [
1291 'admin_tab_group' => $current_tabs_group,
1292 ];
1293 $operator = 'and';
1294
1295 if ( empty( $current_tabs_group ) ) {
1296 // Don't include 'not-supported' or other templates that don't set their `admin_tab_group`.
1297 $operator = 'NOT';
1298 }
1299
1300 $doc_types = Plugin::$instance->documents->get_document_types( $filter, $operator );
1301
1302 if ( 1 >= count( $doc_types ) ) {
1303 return $views;
1304 }
1305
1306 ?>
1307 <div id="elementor-template-library-tabs-wrapper" class="nav-tab-wrapper">
1308 <a class="nav-tab<?php echo esc_attr( $active_class ); ?>" href="<?php echo esc_url( $baseurl ); ?>">
1309 <?php
1310 $all_title = $this->get_library_title();
1311 if ( ! $all_title ) {
1312 $all_title = esc_html__( 'All', 'elementor' );
1313 }
1314 Utils::print_unescaped_internal_string( $all_title ); ?>
1315 </a>
1316 <?php
1317 foreach ( $doc_types as $type => $class_name ) :
1318 $active_class = '';
1319
1320 if ( $current_type === $type ) {
1321 $active_class = ' nav-tab-active';
1322 }
1323
1324 $type_url = esc_url( add_query_arg( self::TAXONOMY_TYPE_SLUG, $type, $baseurl ) );
1325 $type_label = $this->get_template_label_by_type( $type );
1326 Utils::print_unescaped_internal_string( "<a class='nav-tab{$active_class}' href='{$type_url}'>{$type_label}</a>" );
1327 endforeach;
1328 ?>
1329 </div>
1330 <?php
1331 return $views;
1332 }
1333
1334 /**
1335 * Maybe render blank state.
1336 *
1337 * When the template library has no saved templates, display a blank admin page offering
1338 * to create the very first template.
1339 *
1340 * Fired by `manage_posts_extra_tablenav` action.
1341 *
1342 * @since 2.0.0
1343 * @access public
1344 *
1345 * @param string $which The location of the extra table nav markup: 'top' or 'bottom'.
1346 * @param array $args
1347 */
1348 public function maybe_render_blank_state( $which, array $args = [] ) {
1349 global $post_type;
1350
1351 $args = wp_parse_args( $args, [
1352 'cpt' => self::CPT,
1353 'post_type' => get_query_var( 'elementor_library_type' ),
1354 ] );
1355
1356 if ( $args['cpt'] !== $post_type || 'bottom' !== $which ) {
1357 return;
1358 }
1359
1360 global $wp_list_table;
1361
1362 $total_items = $wp_list_table->get_pagination_arg( 'total_items' );
1363
1364 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required to retrieve the value.
1365 if ( ! empty( $total_items ) || ! empty( $_REQUEST['s'] ) ) {
1366 return;
1367 }
1368
1369 $current_type = $args['post_type'];
1370
1371 $document_types = Plugin::instance()->documents->get_document_types();
1372
1373 if ( empty( $document_types[ $current_type ] ) ) {
1374 return;
1375 }
1376
1377 // TODO: Better way to exclude widget type.
1378 if ( 'widget' === $current_type ) {
1379 return;
1380 }
1381
1382 // TODO: This code maybe unreachable see if above `if ( empty( $document_types[ $current_type ] ) )`.
1383 if ( empty( $current_type ) ) {
1384 $counts = (array) wp_count_posts( self::CPT );
1385 unset( $counts['auto-draft'] );
1386 $count = array_sum( $counts );
1387
1388 if ( 0 < $count ) {
1389 return;
1390 }
1391
1392 $current_type = 'template';
1393
1394 $args['additional_inline_style'] = '#elementor-template-library-tabs-wrapper {display: none;}';
1395 }
1396
1397 $this->render_blank_state( $current_type, $args );
1398 }
1399
1400 private function render_blank_state( $current_type, array $args = [] ) {
1401 $current_type_label = $this->get_template_label_by_type( $current_type );
1402 $inline_style = '#posts-filter .wp-list-table, #posts-filter .tablenav.top, .tablenav.bottom .actions, .wrap .subsubsub { display:none;}';
1403
1404 $args = wp_parse_args( $args, [
1405 'additional_inline_style' => '',
1406 'href' => '',
1407 'description' => esc_html__( 'Add templates and reuse them across your website. Easily export and import them to any other project, for an optimized workflow.', 'elementor' ),
1408 ] );
1409 $inline_style .= $args['additional_inline_style'];
1410 ?>
1411 <style type="text/css"><?php Utils::print_unescaped_internal_string( $inline_style ); ?></style>
1412 <div class="elementor-template_library-blank_state">
1413 <?php $this->print_blank_state_template( $current_type_label, $args['href'], $args['description'] ); ?>
1414 </div>
1415 <?php
1416 }
1417
1418 /**
1419 * Print Blank State Template
1420 *
1421 * When the an entity (CPT, Taxonomy...etc) has no saved items, print a blank admin page offering
1422 * to create the very first item.
1423 *
1424 * This method is public because it needs to be accessed from outside the Source_Local
1425 *
1426 * @since 3.1.0
1427 * @access public
1428 *
1429 * @param string $current_type_label The Entity title.
1430 * @param string $href The URL for the 'Add New' button.
1431 * @param string $description The sub title describing the Entity (Post Type, Taxonomy, etc.).
1432 */
1433 public function print_blank_state_template( $current_type_label, $href, $description ) {
1434 ?>
1435 <div class="elementor-blank_state">
1436 <i class="eicon-folder"></i>
1437 <h3>
1438 <?php
1439 /* translators: %s: Template type label. */
1440 printf( esc_html__( 'Create Your First %s', 'elementor' ), esc_html( $current_type_label ) );
1441 ?>
1442 </h3>
1443 <p><?php echo wp_kses_post( $description ); ?></p>
1444 <a id="elementor-template-library-add-new" class="elementor-button e-primary" href="<?php echo esc_url( $href ); ?>">
1445 <?php
1446 /* translators: %s: Template type label. */
1447 printf( esc_html__( 'Add New %s', 'elementor' ), esc_html( $current_type_label ) );
1448 ?>
1449 </a>
1450 </div>
1451 <?php
1452 }
1453
1454 /**
1455 * Add filter by category.
1456 *
1457 * In the templates library, add a filter by Elementor library category.
1458 *
1459 * @access public
1460 *
1461 * @param string $post_type The post type slug.
1462 */
1463 public function add_filter_by_category( $post_type ) {
1464 if ( self::CPT !== $post_type ) {
1465 return;
1466 }
1467
1468 $all_items = get_taxonomy( self::TAXONOMY_CATEGORY_SLUG )->labels->all_items;
1469 $dropdown_options = [
1470 'show_option_all' => $all_items,
1471 'show_option_none' => $all_items,
1472 'hide_empty' => 0,
1473 'hierarchical' => 1,
1474 'show_count' => 0,
1475 'orderby' => 'name',
1476 'value_field' => 'slug',
1477 'taxonomy' => self::TAXONOMY_CATEGORY_SLUG,
1478 'name' => self::TAXONOMY_CATEGORY_SLUG,
1479 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required to retrieve the value.
1480 'selected' => Utils::get_super_global_value( $_GET, self::TAXONOMY_CATEGORY_SLUG ) ?? '',
1481 ];
1482
1483 printf(
1484 '<label class="screen-reader-text" for="%1$s">%2$s</label>',
1485 esc_attr( self::TAXONOMY_CATEGORY_SLUG ),
1486 esc_html_x( 'Filter by category', 'Template Library', 'elementor' )
1487 );
1488 wp_dropdown_categories( $dropdown_options );
1489 }
1490
1491 /**
1492 * Import single template.
1493 *
1494 * Import template from a file to the database.
1495 *
1496 * @since 1.6.0
1497 * @access private
1498 *
1499 * @param string $file_path File name.
1500 *
1501 * @return \WP_Error|int|array Local template array, or template ID, or
1502 * `WP_Error`.
1503 */
1504 private function import_single_template( $file_path ) {
1505 $data = $this->prepare_import_template_data( $file_path );
1506
1507 if ( is_wp_error( $data ) ) {
1508 return $data;
1509 }
1510
1511 if ( empty( $data ) ) {
1512 return new \WP_Error( 'file_error', 'Invalid File' );
1513 }
1514
1515 $template_id = $this->save_item( [
1516 'content' => $data['content'],
1517 'title' => $data['title'],
1518 'type' => $data['type'],
1519 'page_settings' => $data['page_settings'],
1520 ] );
1521
1522 if ( is_wp_error( $template_id ) ) {
1523 return $template_id;
1524 }
1525
1526 return $this->get_item( $template_id );
1527 }
1528
1529 /**
1530 * Prepare template to export.
1531 *
1532 * Retrieve the relevant template data and return them as an array.
1533 *
1534 * @since 1.6.0
1535 * @access private
1536 *
1537 * @param int $template_id The template ID.
1538 *
1539 * @return \WP_Error|array Exported template data.
1540 */
1541 private function prepare_template_export( $template_id ) {
1542 $document = Plugin::$instance->documents->get( $template_id );
1543
1544 $template_data = $document->get_export_data();
1545
1546 if ( empty( $template_data['content'] ) ) {
1547 return new \WP_Error( 'empty_template', 'The template is empty' );
1548 }
1549
1550 $content = apply_filters(
1551 'elementor/template_library/sources/local/export/elements',
1552 $template_data['content']
1553 );
1554
1555 $export_data = [
1556 'content' => $content,
1557 'page_settings' => $template_data['settings'],
1558 'version' => DB::DB_VERSION,
1559 'title' => $document->get_main_post()->post_title,
1560 'type' => self::get_template_type( $template_id ),
1561 ];
1562
1563 return [
1564 'name' => 'elementor-' . $template_id . '-' . gmdate( 'Y-m-d' ) . '.json',
1565 'content' => wp_json_encode( $export_data ),
1566 ];
1567 }
1568
1569 /**
1570 * Get template label by type.
1571 *
1572 * Retrieve the template label for any given template type.
1573 *
1574 * @since 2.0.0
1575 * @access private
1576 *
1577 * @param string $template_type Template type.
1578 *
1579 * @return string Template label.
1580 */
1581 private function get_template_label_by_type( $template_type ) {
1582 $document_types = Plugin::instance()->documents->get_document_types();
1583
1584 if ( isset( $document_types[ $template_type ] ) ) {
1585 $template_label = call_user_func( [ $document_types[ $template_type ], 'get_title' ] );
1586 } else {
1587 $template_label = ucwords( str_replace( [ '_', '-' ], ' ', $template_type ) );
1588 }
1589
1590 /**
1591 * Template label by template type.
1592 *
1593 * Filters the template label by template type in the template library .
1594 *
1595 * @since 2.0.0
1596 *
1597 * @param string $template_label Template label.
1598 * @param string $template_type Template type.
1599 */
1600 $template_label = apply_filters( 'elementor/template-library/get_template_label_by_type', $template_label, $template_type );
1601
1602 return $template_label;
1603 }
1604
1605 /**
1606 * Filter template types in admin query.
1607 *
1608 * Update the template types in the main admin query.
1609 *
1610 * Fired by `parse_query` action.
1611 *
1612 * @since 2.4.0
1613 * @access public
1614 *
1615 * @param \WP_Query $query The `WP_Query` instance.
1616 */
1617 public function admin_query_filter_types( \WP_Query $query ) {
1618 if ( ! $this->is_current_screen() || ! empty( $query->query_vars['meta_key'] ) ) {
1619 return;
1620 }
1621
1622 $current_tabs_group = $this->get_current_tab_group();
1623
1624 if ( isset( $query->query_vars[ self::TAXONOMY_CATEGORY_SLUG ] ) && '-1' === $query->query_vars[ self::TAXONOMY_CATEGORY_SLUG ] ) {
1625 unset( $query->query_vars[ self::TAXONOMY_CATEGORY_SLUG ] );
1626 }
1627
1628 if ( empty( $current_tabs_group ) ) {
1629 return;
1630 }
1631
1632 $doc_types = Plugin::$instance->documents->get_document_types( [
1633 'admin_tab_group' => $current_tabs_group,
1634 ] );
1635
1636 $query->query_vars['meta_key'] = Document::TYPE_META_KEY;
1637 $query->query_vars['meta_value'] = array_keys( $doc_types );
1638 }
1639
1640 /**
1641 * Add template library actions.
1642 *
1643 * Register filters and actions for the template library.
1644 *
1645 * @since 2.0.0
1646 * @access private
1647 */
1648 private function add_actions() {
1649 if ( is_admin() ) {
1650 add_action( 'elementor/admin/menu/register', function ( Admin_Menu_Manager $admin_menu ) {
1651 $this->register_admin_menu( $admin_menu );
1652 }, static::ADMIN_MENU_PRIORITY );
1653
1654 add_action( 'elementor/admin/menu/register', function ( Admin_Menu_Manager $admin_menu ) {
1655 $this->admin_menu_reorder( $admin_menu );
1656 }, 800 );
1657
1658 add_action( 'elementor/admin/menu/after_register', function () {
1659 $this->admin_menu_set_current();
1660 } );
1661
1662 add_filter( 'admin_title', [ $this, 'admin_title' ], 10, 2 );
1663 add_action( 'all_admin_notices', [ $this, 'replace_admin_heading' ] );
1664 add_filter( 'post_row_actions', [ $this, 'post_row_actions' ], 10, 2 );
1665 add_action( 'admin_footer', [ $this, 'admin_import_template_form' ] );
1666 add_action( 'save_post', [ $this, 'on_save_post' ], 10, 2 );
1667 add_filter( 'display_post_states', [ $this, 'remove_elementor_post_state_from_library' ], 11, 2 );
1668
1669 add_action( 'parse_query', [ $this, 'admin_query_filter_types' ] );
1670
1671 // Template filter by category.
1672 add_action( 'restrict_manage_posts', [ $this, 'add_filter_by_category' ] );
1673
1674 // Template type column.
1675 add_action( 'manage_' . self::CPT . '_posts_columns', [ $this, 'admin_columns_headers' ] );
1676 add_action( 'manage_' . self::CPT . '_posts_custom_column', [ $this, 'admin_columns_content' ], 10, 2 );
1677
1678 // Template library bulk actions.
1679 add_filter( 'bulk_actions-edit-elementor_library', [ $this, 'admin_add_bulk_export_action' ] );
1680 add_filter( 'handle_bulk_actions-edit-elementor_library', [ $this, 'admin_export_multiple_templates' ], 10, 3 );
1681
1682 // Print template library tabs.
1683 add_filter( 'views_edit-' . self::CPT, [ $this, 'admin_print_tabs' ] );
1684
1685 // Show blank state.
1686 add_action( 'manage_posts_extra_tablenav', [ $this, 'maybe_render_blank_state' ] );
1687 }
1688
1689 add_action( 'elementor/document/after_save', [ $this, 'on_template_update' ], 10, 2 );
1690
1691 add_action( 'template_redirect', [ $this, 'block_template_frontend' ] );
1692
1693 // Remove elementor library templates from WP Sitemap
1694 add_filter(
1695 'wp_sitemaps_post_types',
1696 function( $post_types ) {
1697 return $this->remove_elementor_cpt_from_sitemap( $post_types );
1698 }
1699 );
1700 }
1701
1702 public function on_template_update( \Elementor\Core\Base\Document $document, array $data ) {
1703 if ( ! empty( $data['post_title'] ) ) {
1704 wp_update_post( [
1705 'ID' => $document->get_main_id(),
1706 'post_title' => $data['post_title'],
1707 ] );
1708 }
1709 }
1710
1711 /**
1712 * @since 2.0.6
1713 * @access public
1714 */
1715 public function admin_columns_content( $column_name, $post_id ) {
1716 if ( 'elementor_library_type' === $column_name ) {
1717 /** @var Document $document */
1718 $document = Plugin::$instance->documents->get( $post_id );
1719
1720 if ( $document && $document instanceof Library_Document ) {
1721 $document->print_admin_column_type();
1722 }
1723 }
1724 }
1725
1726 /**
1727 * @since 2.0.6
1728 * @access public
1729 */
1730 public function admin_columns_headers( $posts_columns ) {
1731 // Replace original column that bind to the taxonomy - with another column.
1732 unset( $posts_columns['taxonomy-elementor_library_type'] );
1733
1734 $offset = 2;
1735
1736 $posts_columns = array_slice( $posts_columns, 0, $offset, true ) + [
1737 'elementor_library_type' => esc_html__( 'Type', 'elementor' ),
1738 ] + array_slice( $posts_columns, $offset, null, true );
1739
1740 return $posts_columns;
1741 }
1742
1743 public function get_current_tab_group() {
1744 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
1745 $current_tabs_group = Utils::get_super_global_value( $_REQUEST, 'tabs_group' ) ?? '';
1746 //phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not required here.
1747 $type_slug = Utils::get_super_global_value( $_REQUEST, self::TAXONOMY_TYPE_SLUG );
1748
1749 if ( $type_slug ) {
1750 $doc_type = Plugin::$instance->documents->get_document_type( $type_slug, '' );
1751 if ( $doc_type ) {
1752 $current_tabs_group = $doc_type::get_property( 'admin_tab_group' );
1753 }
1754 }
1755 return $current_tabs_group;
1756 }
1757
1758 private function get_library_title() {
1759 $title = '';
1760
1761 if ( $this->is_current_screen() ) {
1762 $current_tab_group = $this->get_current_tab_group();
1763
1764 if ( $current_tab_group ) {
1765 $titles = [
1766 'library' => esc_html__( 'Saved Templates', 'elementor' ),
1767 'theme' => esc_html__( 'Theme Builder', 'elementor' ),
1768 'popup' => esc_html__( 'Popups', 'elementor' ),
1769 ];
1770
1771 if ( ! empty( $titles[ $current_tab_group ] ) ) {
1772 $title = $titles[ $current_tab_group ];
1773 }
1774 }
1775 }
1776
1777 return $title;
1778 }
1779
1780 private function is_current_screen() {
1781 global $pagenow, $typenow;
1782
1783 return 'edit.php' === $pagenow && self::CPT === $typenow;
1784 }
1785
1786 /**
1787 * @param array $post_types
1788 *
1789 * @return array
1790 */
1791 private function remove_elementor_cpt_from_sitemap( array $post_types ) {
1792 unset( $post_types[ self::CPT ] );
1793
1794 return $post_types;
1795 }
1796
1797 private function avoid_rest_access_for_non_admins(): void {
1798 add_filter( 'rest_pre_dispatch', function ( $value, \WP_REST_Server $server, \WP_REST_Request $request ) {
1799 if ( strpos( $request->get_route(), self::CPT ) !== false ) {
1800 if ( ! current_user_can( 'manage_options' ) ) {
1801 return new \WP_Error( 'rest_forbidden', esc_html__( 'Sorry, you are not allowed to do that.', 'elementor' ), [ 'status' => rest_authorization_required_code() ] );
1802 }
1803 }
1804
1805 return $value;
1806 }, 10, 3 );
1807 }
1808
1809 public function save_bulk_items( array $args = [] ) {
1810 $items = [];
1811
1812 foreach ( $args as $item ) {
1813 $items[] = $this->save_item( [
1814 'content' => $item['content'],
1815 'title' => $item['title'],
1816 'type' => $item['type'],
1817 'page_settings' => $item['page_settings'],
1818 ] );
1819 }
1820
1821 return $items;
1822 }
1823
1824 /**
1825 * Template library local source constructor.
1826 *
1827 * Initializing the template library local source base by registering custom
1828 * template data and running custom actions.
1829 *
1830 * @since 1.0.0
1831 * @access public
1832 */
1833 public function __construct() {
1834 parent::__construct();
1835
1836 $this->add_actions();
1837 }
1838
1839 public function format_args_for_bulk_action( $args ) {
1840 $bulk_args = [];
1841
1842 foreach ( $args['from_template_id'] as $from_template_id ) {
1843 if ( ! $this->is_allowed_to_read_template( [
1844 'template_id' => $from_template_id,
1845 ] ) ) {
1846 continue;
1847 }
1848
1849 $document = Plugin::$instance->documents->get( $from_template_id );
1850
1851 if ( ! $document ) {
1852 continue;
1853 }
1854
1855 $page = SettingsManager::get_settings_managers( 'page' )->get_model( $from_template_id );
1856
1857 $bulk_args[] = array_merge(
1858 $args,
1859 [
1860 'title' => $document->get_post()->post_title,
1861 'type' => $document::get_type(),
1862 'content' => $document->get_elements_data(),
1863 'page_settings' => $page->get_data( 'settings' ),
1864 ]
1865 );
1866 }
1867
1868 return $bulk_args;
1869 }
1870
1871 public function format_args_for_single_action( $args ) {
1872 if ( ! $this->is_allowed_to_read_template( [
1873 'template_id' => $args['from_template_id'],
1874 ] ) ) {
1875 return new \WP_Error(
1876 'template_error',
1877 esc_html__( 'You do not have permission to access this template.', 'elementor' )
1878 );
1879 }
1880
1881 $document = Plugin::$instance->documents->get( $args['from_template_id'] );
1882
1883 if ( ! $document ) {
1884 return new \WP_Error( 'template_error', 'Document not found.' );
1885 }
1886
1887 $args['content'] = $document->get_elements_data();
1888
1889 $page = SettingsManager::get_settings_managers( 'page' )->get_model( $args['from_template_id'] );
1890 $args['page_settings'] = $page->get_data( 'settings' );
1891
1892 return $args;
1893 }
1894
1895 public function is_allowed_to_read_template( array $args ): bool {
1896 if ( null === $this->wordpress_adapter ) {
1897 $this->set_wordpress_adapter( new WordPress_Adapter() );
1898 }
1899
1900 if ( ! $this->should_check_permissions( $args ) ) {
1901 return true;
1902 }
1903
1904 $post_id = intval( $args['template_id'] );
1905 $post_status = $this->wordpress_adapter->get_post_status( $post_id );
1906 $is_private_or_non_published = ( 'private' === $post_status && ! $this->wordpress_adapter->current_user_can( 'read_private_posts', $post_id ) ) || ( 'publish' !== $post_status );
1907
1908 $can_read_template = $is_private_or_non_published || $this->wordpress_adapter->current_user_can( 'edit_post', $post_id );
1909
1910 return apply_filters( 'elementor/template-library/is_allowed_to_read_template', $can_read_template, $args );
1911 }
1912
1913 private function should_check_permissions( array $args ): bool {
1914 if ( null === $this->elementor_adapter ) {
1915 $this->set_elementor_adapter( new Elementor_Adapter() );
1916 }
1917
1918 $check_permissions = isset( $args['check_permissions'] ) && false === $args['check_permissions'];
1919
1920 if ( $check_permissions ) {
1921 return false;
1922 }
1923
1924 return true;
1925 }
1926
1927 public function set_wordpress_adapter( Wordpress_Adapter_Interface $wordpress_adapter ) {
1928 $this->wordpress_adapter = $wordpress_adapter;
1929 }
1930
1931 public function set_elementor_adapter( Elementor_Adapter_Interface $elementor_adapter ): void {
1932 $this->elementor_adapter = $elementor_adapter;
1933 }
1934 }
1935