PluginProbe
Elementor Website Builder – more than just a page builder / 4.2.0
Elementor Website Builder – more than just a page builder v4.2.0
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 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / includes / template-library / sources / local.php

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

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