PluginProbe
Elementor Website Builder – more than just a page builder / 4.0.0-beta1
Elementor Website Builder – more than just a page builder v4.0.0-beta1
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 / manager.php

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

1,235 lines 31.7 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\Api;
5 use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
6 use Elementor\Core\Settings\Manager as SettingsManager;
7 use Elementor\Includes\TemplateLibrary\Data\Controller;
8 use Elementor\TemplateLibrary\Classes\Import_Images;
9 use Elementor\Core\Utils\Template_Library_Import_Export_Utils;
10 use Elementor\Plugin;
11 use Elementor\User;
12 use Elementor\Utils;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit; // Exit if accessed directly.
16 }
17
18 /**
19 * Elementor template library manager.
20 *
21 * Elementor template library manager handler class is responsible for
22 * initializing the template library.
23 *
24 * @since 1.0.0
25 */
26 class Manager {
27
28 const ERROR_TEMPLATE_SOURCE_NOT_FOUND = 'Template source not found.';
29 const ERROR_TEMPLATE_IDS_MISSING = 'Template IDs are missing.';
30
31 /**
32 * Registered template sources.
33 *
34 * Holds a list of all the supported sources with their instances.
35 *
36 * @access protected
37 *
38 * @var Source_Base[]
39 */
40 protected $_registered_sources = []; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
41
42 /**
43 * Imported template images.
44 *
45 * Holds an instance of `Import_Images` class.
46 *
47 * @access private
48 *
49 * @var Import_Images
50 */
51 private $_import_images = null; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
52
53 /**
54 * Template library manager constructor.
55 *
56 * Initializing the template library manager by registering default template
57 * sources and initializing ajax calls.
58 *
59 * @since 1.0.0
60 * @access public
61 */
62 public function __construct() {
63 Plugin::$instance->data_manager_v2->register_controller( new Controller() );
64
65 $this->register_default_sources();
66
67 $this->add_actions();
68 }
69
70 /**
71 * @since 2.3.0
72 * @access public
73 */
74 public function add_actions() {
75 add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
76 add_action( 'wp_ajax_elementor_library_direct_actions', [ $this, 'handle_direct_actions' ] );
77 }
78
79 /**
80 * Get `Import_Images` instance.
81 *
82 * Retrieve the instance of the `Import_Images` class.
83 *
84 * @since 1.0.0
85 * @access public
86 *
87 * @return Import_Images Imported images instance.
88 */
89 public function get_import_images_instance() {
90 if ( null === $this->_import_images ) {
91 $this->_import_images = new Import_Images();
92 }
93
94 return $this->_import_images;
95 }
96
97 /**
98 * Register template source.
99 *
100 * Used to register new template sources displayed in the template library.
101 *
102 * @since 1.0.0
103 * @access public
104 *
105 * @param string $source_class The name of source class.
106 * @param array $args Optional. Class arguments. Default is an
107 * empty array.
108 *
109 * @return \WP_Error|true True if the source was registered, `WP_Error`
110 * otherwise.
111 */
112 public function register_source( $source_class, $args = [] ) {
113 if ( ! class_exists( $source_class ) ) {
114 return new \WP_Error( 'source_class_name_not_exists' );
115 }
116
117 $source_instance = new $source_class( $args );
118
119 if ( ! $source_instance instanceof Source_Base ) {
120 return new \WP_Error( 'wrong_instance_source' );
121 }
122
123 $source_id = $source_instance->get_id();
124
125 if ( isset( $this->_registered_sources[ $source_id ] ) ) {
126 return new \WP_Error( 'source_exists' );
127 }
128
129 $this->_registered_sources[ $source_id ] = $source_instance;
130
131 return true;
132 }
133
134 /**
135 * Unregister template source.
136 *
137 * Remove an existing template sources from the list of registered template
138 * sources.
139 *
140 * @since 1.0.0
141 * @deprecated 2.7.0
142 * @access public
143 *
144 * @param string $id The source ID.
145 *
146 * @return bool Whether the source was unregistered.
147 */
148 public function unregister_source( $id ) {
149 return true;
150 }
151
152 /**
153 * Get registered template sources.
154 *
155 * Retrieve registered template sources.
156 *
157 * @since 1.0.0
158 * @access public
159 *
160 * @return Source_Base[] Registered template sources.
161 */
162 public function get_registered_sources() {
163 return $this->_registered_sources;
164 }
165
166 /**
167 * Get template source.
168 *
169 * Retrieve single template sources for a given template ID.
170 *
171 * @since 1.0.0
172 * @access public
173 *
174 * @param string $id The source ID.
175 *
176 * @return false|Source_Base Template sources if one exist, False otherwise.
177 */
178 public function get_source( $id ) {
179 $sources = $this->get_registered_sources();
180
181 if ( ! isset( $sources[ $id ] ) ) {
182 return false;
183 }
184
185 return $sources[ $id ];
186 }
187
188 /**
189 * Get templates.
190 *
191 * Retrieve all the templates from all the registered sources.
192 *
193 * @param array $filter_sources
194 * @param bool $force_update
195 * @return array
196 */
197 public function get_templates( array $filter_sources = [], bool $force_update = false ): array {
198 $templates = [];
199
200 foreach ( $this->get_registered_sources() as $source ) {
201 if ( ! empty( $filter_sources ) && ! in_array( $source->get_id(), $filter_sources, true ) ) {
202 continue;
203 }
204
205 $templates = array_merge( $templates, $source->get_items( [ 'force_update' => $force_update ] ) );
206 }
207
208 return $templates;
209 }
210
211 /**
212 * Get library data.
213 *
214 * Retrieve the library data.
215 *
216 * @since 1.9.0
217 * @access public
218 *
219 * @param array $args Library arguments.
220 *
221 * @return array Library data.
222 */
223 public function get_library_data( array $args ) {
224 $force_update = ! empty( $args['sync'] );
225
226 $library_data = Api::get_library_data( $force_update );
227
228 if ( empty( $library_data ) ) {
229 return $library_data;
230 }
231
232 // Ensure all document are registered.
233 Plugin::$instance->documents->get_document_types();
234
235 $filter_sources = ! empty( $args['filter_sources'] ) ? $args['filter_sources'] : [];
236
237 $full_library_data = [
238 'templates' => $this->get_templates( $filter_sources, $force_update ),
239 'config' => $library_data['types_data'],
240 ];
241
242 /**
243 * Filter the full library data.
244 *
245 * @since 3.32.2
246 * @param-out $full_library_data - 'templates' and 'config' data ('config' holds the list of categories).
247 */
248 return apply_filters( 'elementor/library/full-data', $full_library_data );
249 }
250
251 /**
252 * Save template.
253 *
254 * Save new or update existing template on the database.
255 *
256 * @since 1.0.0
257 * @access public
258 *
259 * @param array $args Template arguments.
260 *
261 * @return \WP_Error|int The ID of the saved/updated template.
262 */
263 public function save_template( array $args ) {
264 $validate_args = $this->ensure_args( [ 'post_id', 'source', 'content', 'type' ], $args );
265
266 if ( is_wp_error( $validate_args ) ) {
267 return $validate_args;
268 }
269
270 $sources = (array) $args['source']; // BC
271 $results = [];
272
273 foreach ( $sources as $source ) {
274 $args_copy = $args;
275 $args_copy['source'] = $source;
276 $results[] = $this->save_template_item( $args_copy );
277 }
278
279 return 1 === count( $results ) ? $results[0] : $results;
280 }
281
282 private function save_template_item( array $args ) {
283 $source = $this->get_source( $args['source'] );
284
285 if ( ! $source ) {
286 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
287 }
288
289 $args['content'] = json_decode( $args['content'], true );
290
291 $page = SettingsManager::get_settings_managers( 'page' )->get_model( $args['post_id'] );
292
293 $args['page_settings'] = $page->get_data( 'settings' );
294
295 $template_id = $source->save_item( $args );
296
297 if ( is_wp_error( $template_id ) ) {
298 return $template_id;
299 }
300
301 return $source->get_item( $template_id );
302 }
303
304 public function move_template( array $args ) {
305 $validate_args = $this->ensure_args( [ 'source', 'from_source', 'from_template_id' ], $args );
306
307 if ( is_wp_error( $validate_args ) ) {
308 return $validate_args;
309 }
310
311 $args['source'] = $args['source'][0];
312
313 $result = $this->move_template_item( $args );
314
315 if ( ! $this->is_action_to_same_source( $args ) ) {
316 $this->delete_template( [
317 'source' => $args['from_source'],
318 'template_id' => $args['from_template_id'],
319 ] );
320 }
321
322 return $result;
323 }
324
325 private function move_template_item( array $args ) {
326 $validate_args = $this->ensure_args( [ 'source', 'from_source', 'from_template_id' ], $args );
327
328 if ( is_wp_error( $validate_args ) ) {
329 return $validate_args;
330 }
331
332 $source = $this->get_source( $args['source'] );
333
334 if ( ! $source ) {
335 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
336 }
337
338 if ( $this->is_action_to_same_source( $args ) ) {
339 return $source->move_template_to_folder( $args );
340 }
341
342 $from_source = $this->get_source( $args['from_source'] );
343
344 if ( ! $from_source ) {
345 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
346 }
347
348 $args = $from_source->format_args_for_single_action( $args );
349
350 $template_id = $source->save_item( $args );
351
352 if ( is_wp_error( $template_id ) ) {
353 return $template_id;
354 }
355
356 return $source->get_item( $template_id );
357 }
358
359 public function copy_template( array $args ) {
360 $validate_args = $this->ensure_args( [ 'source', 'from_source', 'from_template_id' ], $args );
361
362 if ( is_wp_error( $validate_args ) ) {
363 return $validate_args;
364 }
365
366 $source = $this->get_source( $args['source'][0] );
367
368 if ( ! $source ) {
369 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
370 }
371
372 $from_source = $this->get_source( $args['from_source'] );
373
374 if ( ! $from_source ) {
375 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
376 }
377
378 $args = $from_source->format_args_for_single_action( $args );
379
380 $template_id = $source->save_item( $args );
381
382 if ( is_wp_error( $template_id ) ) {
383 return $template_id;
384 }
385
386 return $source->get_item( $template_id );
387 }
388
389 private function is_action_to_same_source( $args ) {
390 return $args['source'] === $args['from_source'];
391 }
392
393 /**
394 * Update template.
395 *
396 * Update template on the database.
397 *
398 * @since 1.0.0
399 * @access public
400 *
401 * @param array $template_data New template data.
402 *
403 * @return \WP_Error|Source_Base Template sources instance if the templates
404 * was updated, `WP_Error` otherwise.
405 */
406 public function update_template( array $template_data ) {
407 $validate_args = $this->ensure_args( [ 'source', 'content', 'type' ], $template_data );
408
409 if ( is_wp_error( $validate_args ) ) {
410 return $validate_args;
411 }
412
413 $source = $this->get_source( $template_data['source'] );
414
415 if ( ! $source ) {
416 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
417 }
418
419 $template_data['content'] = json_decode( $template_data['content'], true );
420
421 $update = $source->update_item( $template_data );
422
423 if ( is_wp_error( $update ) ) {
424 return $update;
425 }
426
427 return $source->get_item( $template_data['id'] );
428 }
429
430 public function rename_template( array $template_data ) {
431 $validate_args = $this->ensure_args( [ 'source', 'title', 'id' ], $template_data );
432
433 if ( is_wp_error( $validate_args ) ) {
434 return $validate_args;
435 }
436
437 $source = $this->get_source( $template_data['source'] );
438
439 if ( ! $source ) {
440 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
441 }
442
443 $update = $source->update_item( $template_data );
444
445 if ( is_wp_error( $update ) ) {
446 return $update;
447 }
448
449 return $source->get_item( $template_data['id'] );
450 }
451
452 /**
453 * Update templates.
454 *
455 * Update template on the database.
456 *
457 * @since 1.0.0
458 * @access public
459 *
460 * @param array $args Template arguments.
461 *
462 * @return \WP_Error|true True if templates updated, `WP_Error` otherwise.
463 */
464 public function update_templates( array $args ) {
465 foreach ( $args['templates'] as $template_data ) {
466 $result = $this->update_template( $template_data );
467
468 if ( is_wp_error( $result ) ) {
469 return $result;
470 }
471 }
472
473 return true;
474 }
475
476 /**
477 * Get template data.
478 *
479 * Retrieve the template data.
480 *
481 * @since 1.5.0
482 * @access public
483 *
484 * @param array $args Template arguments.
485 *
486 * @return \WP_Error|bool|array ??
487 */
488 public function get_template_data( array $args ) {
489 $validate_args = $this->ensure_args( [ 'source', 'template_id' ], $args );
490
491 if ( is_wp_error( $validate_args ) ) {
492 return $validate_args;
493 }
494
495 $source = $this->get_source( $args['source'] );
496
497 if ( ! $source ) {
498 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
499 }
500
501 if ( method_exists( $source, 'is_allowed_to_read_template' ) && ! $source->is_allowed_to_read_template( $args ) ) {
502 return new \WP_Error(
503 'template_error',
504 esc_html__( 'You do not have permission to access this template.', 'elementor' )
505 );
506 }
507
508 if ( isset( $args['edit_mode'] ) ) {
509 Plugin::$instance->editor->set_edit_mode( $args['edit_mode'] );
510 }
511
512 do_action( 'elementor/template-library/before_get_source_data', $args, $source );
513
514 $data = $source->get_data( $args );
515
516 do_action( 'elementor/template-library/after_get_source_data', $args, $source );
517
518 return $data;
519 }
520
521 /**
522 * Delete template.
523 *
524 * Delete template from the database.
525 *
526 * @since 1.0.0
527 * @access public
528 *
529 * @param array $args Template arguments.
530 *
531 * @return \WP_Post|\WP_Error|false|null Post data on success, false or null
532 * or 'WP_Error' on failure.
533 */
534 public function delete_template( array $args ) {
535 $validate_args = $this->ensure_args( [ 'source', 'template_id' ], $args );
536
537 if ( is_wp_error( $validate_args ) ) {
538 return $validate_args;
539 }
540
541 $source = $this->get_source( $args['source'] );
542
543 if ( ! $source ) {
544 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
545 }
546
547 return $source->delete_template( $args['template_id'] );
548 }
549
550 /**
551 * Export template.
552 *
553 * Export template to a file after ensuring it is a valid Elementor template
554 * and checking user permissions for private posts.
555 *
556 * @since 1.0.0
557 * @access public
558 *
559 * @param array $args Template arguments.
560 *
561 * @return mixed Whether the export succeeded or failed.
562 */
563 public function export_template( array $args ) {
564 $validate_args = $this->ensure_args( [ 'source', 'template_id' ], $args );
565
566 if ( is_wp_error( $validate_args ) ) {
567 return $validate_args;
568 }
569
570 $source = $this->get_source( $args['source'] );
571
572 if ( ! $source ) {
573 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
574 }
575
576 return $source->export_template( $args['template_id'] );
577 }
578
579 /**
580 * @since 2.3.0
581 * @access public
582 */
583 public function direct_import_template() {
584 /** @var Source_Local $source */
585 $source = $this->get_source( 'local' );
586 $file = Utils::get_super_global_value( $_FILES, 'file' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
587 return $source->import_template( $file['name'], $file['tmp_name'] );
588 }
589
590 /**
591 * Import template.
592 *
593 * Import template from a file.
594 *
595 * @since 1.0.0
596 * @access public
597 *
598 * @param array $data
599 *
600 * @return mixed Whether the export succeeded or failed.
601 */
602 public function import_template( array $data ) {
603 // If the template is a JSON file, allow uploading it.
604 add_filter( 'elementor/files/allow-file-type/json', [ $this, 'enable_json_template_upload' ] );
605 add_filter( 'elementor/files/allow_unfiltered_upload', [ $this, 'enable_json_template_upload' ] );
606
607 // Imported templates can be either JSON files, or Zip files containing multiple JSON files
608 $upload_result = Plugin::$instance->uploads_manager->handle_elementor_upload( $data, [ 'zip', 'json' ] );
609
610 remove_filter( 'elementor/files/allow-file-type/json', [ $this, 'enable_json_template_upload' ] );
611 remove_filter( 'elementor/files/allow_unfiltered_upload', [ $this, 'enable_json_template_upload' ] );
612
613 if ( is_wp_error( $upload_result ) ) {
614 Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $upload_result['tmp_name'] ) );
615
616 return $upload_result;
617 }
618
619 $source = $this->get_source( $data['source'] ?? 'local' );
620
621 $import_mode = $data['import_mode'] ?? 'match_site';
622 $import_result = $source->import_template( $upload_result['name'], $upload_result['tmp_name'], $import_mode );
623
624 // Remove the temporary directory generated for the stream-uploaded file.
625 Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $upload_result['tmp_name'] ) );
626
627 return $import_result;
628 }
629
630 /**
631 * Enable JSON Template Upload
632 *
633 * Runs on the 'elementor/files/allow-file-type/json' Uploads Manager filter.
634 *
635 * @since 3.5.0
636 * @access public
637 *
638 * return bool
639 */
640 public function enable_json_template_upload() {
641 return true;
642 }
643
644 /**
645 * Mark template as favorite.
646 *
647 * Add the template to the user favorite templates.
648 *
649 * @since 1.9.0
650 * @access public
651 *
652 * @param array $args Template arguments.
653 *
654 * @return mixed Whether the template marked as favorite.
655 */
656 public function mark_template_as_favorite( $args ) {
657 $validate_args = $this->ensure_args( [ 'source', 'template_id', 'favorite' ], $args );
658
659 if ( is_wp_error( $validate_args ) ) {
660 return $validate_args;
661 }
662
663 $source = $this->get_source( $args['source'] );
664
665 return $source->mark_as_favorite( $args['template_id'], filter_var( $args['favorite'], FILTER_VALIDATE_BOOLEAN ) );
666 }
667
668 public function import_from_json( array $args ) {
669 $validate_args = $this->ensure_args( [ 'editor_post_id', 'elements' ], $args );
670
671 if ( is_wp_error( $validate_args ) ) {
672 return $validate_args;
673 }
674
675 $elements = json_decode( $args['elements'], true );
676
677 $document = Plugin::$instance->documents->get( $args['editor_post_id'] );
678 if ( ! $document ) {
679 return new \WP_Error( 'template_error', 'Document not found.' );
680 }
681
682 $import_data = $document->get_import_data( [ 'content' => $elements ] );
683
684 return $import_data['content'];
685 }
686
687 public function process_global_styles( array $args ) {
688 $validate_args = $this->ensure_args( [ 'content', 'import_mode' ], $args );
689
690 if ( is_wp_error( $validate_args ) ) {
691 return $validate_args;
692 }
693
694 $import_mode = Template_Library_Import_Export_Utils::sanitize_import_mode( $args['import_mode'] );
695
696 $content = $this->get_validated_json_arg( $args, 'content' );
697 if ( is_wp_error( $content ) ) {
698 return $content;
699 }
700
701 $global_classes = $this->get_validated_json_arg( $args, 'global_classes', true );
702 if ( is_wp_error( $global_classes ) ) {
703 return $global_classes;
704 }
705
706 $global_variables = $this->get_validated_json_arg( $args, 'global_variables', true );
707 if ( is_wp_error( $global_variables ) ) {
708 return $global_variables;
709 }
710
711 $data = [
712 'global_classes' => $global_classes,
713 'global_variables' => $global_variables,
714 ];
715
716 $result = apply_filters(
717 'elementor/template_library/import/process_content',
718 [ 'content' => $content ],
719 $import_mode,
720 $data,
721 null
722 );
723
724 $response = [
725 'content' => $result['content'],
726 ];
727
728 if ( ! empty( $result['updated_global_classes'] ) ) {
729 $response['updated_global_classes'] = $result['updated_global_classes'];
730 }
731
732 if ( ! empty( $result['updated_global_variables'] ) ) {
733 $response['updated_global_variables'] = $result['updated_global_variables'];
734 }
735
736 $classes_to_flatten = $result['classes_to_flatten'] ?? [];
737 $variables_to_flatten = $result['variables_to_flatten'] ?? [];
738
739 if ( ! empty( $classes_to_flatten ) ) {
740 $response['flattened_classes_count'] = count( $classes_to_flatten );
741 }
742
743 if ( ! empty( $variables_to_flatten ) ) {
744 $response['flattened_variables_count'] = count( $variables_to_flatten );
745 }
746
747 return $response;
748 }
749
750 private function get_validated_json_arg( array $args, string $key, bool $is_optional = false ) {
751 if ( ! array_key_exists( $key, $args ) || null === $args[ $key ] || '' === $args[ $key ] ) {
752 return $is_optional ? null : new \WP_Error( "invalid_$key", "Invalid $key." );
753 }
754
755 $value = $args[ $key ];
756 if ( is_string( $value ) ) {
757 $decoded = json_decode( $value, true );
758 if ( JSON_ERROR_NONE !== json_last_error() ) {
759 return new \WP_Error( "invalid_$key", "Invalid JSON $key." );
760 }
761 return $decoded;
762 }
763
764 if ( ! is_array( $value ) ) {
765 return new \WP_Error( "invalid_$key", "Invalid $key format." );
766 }
767
768 return $value;
769 }
770
771 public function get_item_children( array $args ) {
772 $validate_args = $this->ensure_args( [ 'source', 'template_id' ], $args );
773
774 if ( is_wp_error( $validate_args ) ) {
775 return $validate_args;
776 }
777
778 $source = $this->get_source( $args['source'] );
779
780 if ( ! $source ) {
781 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
782 }
783
784 return $source->get_item_children( $args );
785 }
786
787 public function search_templates( array $args ) {
788 $validate_args = $this->ensure_args( [ 'source', 'search' ], $args );
789
790 if ( is_wp_error( $validate_args ) ) {
791 return $validate_args;
792 }
793
794 $source = $this->get_source( $args['source'] );
795
796 if ( ! $source ) {
797 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
798 }
799
800 return $source->search_templates( $args );
801 }
802
803 public function load_more_templates( array $args ) {
804 $validate_args = $this->ensure_args( [ 'source', 'offset' ], $args );
805
806 if ( is_wp_error( $validate_args ) ) {
807 return $validate_args;
808 }
809
810 $source = $this->get_source( $args['source'] );
811
812 if ( ! $source ) {
813 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
814 }
815
816 return $source->get_items( $args );
817 }
818
819 public function create_folder( array $args ) {
820 $validate_args = $this->ensure_args( [ 'source', 'title' ], $args );
821
822 if ( is_wp_error( $validate_args ) ) {
823 return $validate_args;
824 }
825
826 $source = $this->get_source( $args['source'] );
827
828 if ( ! $source ) {
829 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
830 }
831
832 return $source->save_folder( $args );
833 }
834
835 public function get_folders( array $args ) {
836 $validate_args = $this->ensure_args( [ 'source', 'offset' ], $args );
837
838 if ( is_wp_error( $validate_args ) ) {
839 return $validate_args;
840 }
841
842 $source = $this->get_source( $args['source'] );
843
844 if ( ! $source ) {
845 return new \WP_Error( 'template_error', 'Folder source not found.' );
846 }
847
848 $args['templateType'] = 'folder';
849
850 return $source->get_items( $args );
851 }
852
853 /**
854 * Register default template sources.
855 *
856 * Register the 'local' and 'remote' template sources that Elementor use by
857 * default.
858 *
859 * @since 1.0.0
860 * @access private
861 */
862 private function register_default_sources() {
863 $sources = [
864 'local',
865 'remote',
866 'cloud',
867 ];
868
869 foreach ( $sources as $source_filename ) {
870 $class_name = ucwords( $source_filename );
871 $class_name = str_replace( '-', '_', $class_name );
872
873 $this->register_source( __NAMESPACE__ . '\Source_' . $class_name );
874 }
875 }
876
877 /**
878 * Handle ajax request.
879 *
880 * Fire authenticated ajax actions for any given ajax request.
881 *
882 * @since 1.0.0
883 * @access private
884 *
885 * @param string $ajax_request Ajax request.
886 *
887 * @param array $data
888 *
889 * @return mixed
890 * @throws \Exception If current user has no permission or the post is not found.
891 */
892 private function handle_ajax_request( $ajax_request, array $data ) {
893 if ( ! User::is_current_user_can_edit_post_type( Source_Local::CPT ) ) {
894 throw new \Exception( 'Access denied.' );
895 }
896
897 if ( ! empty( $data['editor_post_id'] ) ) {
898 $editor_post_id = absint( $data['editor_post_id'] );
899
900 if ( ! get_post( $editor_post_id ) ) {
901 throw new \Exception( 'Post not found.' );
902 }
903
904 Plugin::$instance->db->switch_to_post( $editor_post_id );
905 }
906
907 $result = call_user_func( [ $this, $ajax_request ], $data );
908
909 if ( is_wp_error( $result ) ) {
910 throw new \Exception( esc_html( $result->get_error_message() ) );
911 }
912
913 return $result;
914 }
915
916 /**
917 * @throws \Exception If template import fails, file validation errors occur, or processing encounters issues.
918 */
919 public function save_template_screenshot( $data ): string {
920 $validate_args = $this->ensure_args( [ 'template_id', 'screenshot' ], $data );
921
922 if ( is_wp_error( $validate_args ) ) {
923 return $validate_args;
924 }
925
926 $raw_binary = base64_decode( substr( $data['screenshot'], strlen( 'data:image/png;base64,' ) ) );
927
928 return $this->get_source( 'cloud' )->save_item_preview( $data['template_id'], $raw_binary );
929 }
930
931 /**
932 * @throws \Exception If template processing fails or data validation errors occur.
933 */
934 public function template_screenshot_failed( $data ): string {
935 $validate_args = $this->ensure_args( [ 'template_id' ], $data );
936
937 if ( is_wp_error( $validate_args ) ) {
938 return $validate_args;
939 }
940
941 return $this->get_source( 'cloud' )->mark_preview_as_failed( $data['template_id'], $data['error'] );
942 }
943
944 public function bulk_delete_templates( $data ) {
945 $validate_args = $this->ensure_args( [ 'template_ids', 'source' ], $data );
946
947 if ( is_wp_error( $validate_args ) ) {
948 return $validate_args;
949 }
950
951 $source = $this->get_source( $data['source'] );
952
953 if ( ! $source ) {
954 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
955 }
956
957 if ( empty( $data['template_ids'] ) || ! is_array( $data['template_ids'] ) ) {
958 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_IDS_MISSING );
959 }
960
961 return $source->bulk_delete_items( $data['template_ids'] );
962 }
963
964 public function bulk_undo_delete_items( $data ) {
965 $validate_args = $this->ensure_args( [ 'template_ids', 'source' ], $data );
966
967 if ( is_wp_error( $validate_args ) ) {
968 return $validate_args;
969 }
970
971 $source = $this->get_source( $data['source'] );
972
973 if ( ! $source ) {
974 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
975 }
976
977 if ( empty( $data['template_ids'] ) || ! is_array( $data['template_ids'] ) ) {
978 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_IDS_MISSING );
979 }
980
981 return $source->bulk_undo_delete_items( $data['template_ids'] );
982 }
983
984 /**
985 * Init ajax calls.
986 *
987 * Initialize template library ajax calls for allowed ajax requests.
988 *
989 * @since 2.3.0
990 * @access public
991 *
992 * @param Ajax $ajax
993 */
994 public function register_ajax_actions( Ajax $ajax ) {
995 $library_ajax_requests = [
996 'get_library_data',
997 'get_template_data',
998 'save_template',
999 'update_templates',
1000 'delete_template',
1001 'import_template',
1002 'mark_template_as_favorite',
1003 'import_from_json',
1004 'get_item_children',
1005 'search_templates',
1006 'rename_template',
1007 'load_more_templates',
1008 'create_folder',
1009 'get_folders',
1010 'save_template_screenshot',
1011 'move_template',
1012 'copy_template',
1013 'bulk_move_templates',
1014 'bulk_delete_templates',
1015 'bulk_copy_templates',
1016 'bulk_undo_delete_items',
1017 'get_templates_quota',
1018 'template_screenshot_failed',
1019 'process_global_styles',
1020 ];
1021
1022 foreach ( $library_ajax_requests as $ajax_request ) {
1023 $ajax->register_ajax_action( $ajax_request, function( $data ) use ( $ajax_request ) {
1024 return $this->handle_ajax_request( $ajax_request, $data );
1025 } );
1026 }
1027 }
1028
1029 /**
1030 * @since 2.3.0
1031 * @access public
1032 */
1033 public function handle_direct_actions() {
1034 if ( ! User::is_current_user_can_edit_post_type( Source_Local::CPT ) ) {
1035 return;
1036 }
1037
1038 /** @var Ajax $ajax */
1039 $ajax = Plugin::$instance->common->get_component( 'ajax' );
1040
1041 if ( ! $ajax->verify_request_nonce() ) {
1042 $this->handle_direct_action_error( 'Access Denied' );
1043 }
1044
1045 $action = Utils::get_super_global_value( $_REQUEST, 'library_action' ); // phpcs:ignore -- Nonce already verified.
1046
1047 $whitelist_methods = [
1048 'export_template',
1049 'direct_import_template',
1050 ];
1051
1052 if ( 'direct_import_template' === $action && ! User::is_current_user_can_upload_json() ) {
1053 return;
1054 }
1055
1056 if ( in_array( $action, $whitelist_methods, true ) ) {
1057 $result = $this->$action( $_REQUEST ); // phpcs:ignore -- Nonce already verified.
1058 } else {
1059 $result = new \WP_Error( 'method_not_exists', 'Method Not exists' );
1060 }
1061
1062 if ( is_wp_error( $result ) ) {
1063 /** @var \WP_Error $result */
1064 $this->handle_direct_action_error( $result->get_error_message() . '.' );
1065 }
1066
1067 $callback = "on_{$action}_success";
1068
1069 if ( method_exists( $this, $callback ) ) {
1070 $this->$callback( $result );
1071 }
1072
1073 die;
1074 }
1075
1076 /**
1077 * On successful template import.
1078 *
1079 * Redirect the user to the template library after template import was
1080 * successful finished.
1081 *
1082 * @since 2.3.0
1083 * @access private
1084 */
1085 private function on_direct_import_template_success() {
1086 wp_safe_redirect( admin_url( Source_Local::ADMIN_MENU_SLUG ) );
1087 }
1088
1089 /**
1090 * @since 2.3.0
1091 * @access private
1092 */
1093 private function handle_direct_action_error( $message ) {
1094 _default_wp_die_handler( $message, 'Elementor Library' );
1095 }
1096
1097 /**
1098 * Ensure arguments exist.
1099 *
1100 * Checks whether the required arguments exist in the specified arguments.
1101 *
1102 * @since 1.0.0
1103 * @access private
1104 *
1105 * @param array $required_args Required arguments to check whether they
1106 * exist.
1107 * @param array $specified_args The list of all the specified arguments to
1108 * check against.
1109 *
1110 * @return \WP_Error|true True on success, 'WP_Error' otherwise.
1111 */
1112 private function ensure_args( array $required_args, array $specified_args ) {
1113 $not_specified_args = array_diff( $required_args, array_keys( $specified_args ) );
1114
1115 if ( $not_specified_args ) {
1116 return new \WP_Error( 'arguments_not_specified', sprintf( 'The required argument(s) "%s" not specified.', implode( ', ', $not_specified_args ) ) );
1117 }
1118
1119 return true;
1120 }
1121
1122 public function bulk_move_templates( array $args ) {
1123 $validate_args = $this->ensure_args( [ 'source', 'from_source', 'from_template_id' ], $args );
1124
1125 if ( is_wp_error( $validate_args ) ) {
1126 return $validate_args;
1127 }
1128
1129 $args['source'] = $args['source'][0];
1130
1131 return $this->bulk_move_template_items( $args );
1132 }
1133
1134 private function bulk_move_template_items( array $args ) {
1135 $source = $this->get_source( $args['source'] );
1136
1137 if ( ! $source ) {
1138 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
1139 }
1140
1141 if ( $this->is_action_to_same_source( $args ) ) {
1142 return $source->move_bulk_templates_to_folder( $args );
1143 }
1144
1145 $from_source = $this->get_source( $args['from_source'] );
1146
1147 if ( ! $from_source ) {
1148 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
1149 }
1150
1151 $bulk_args = $from_source->format_args_for_bulk_action( $args );
1152
1153 if ( $source->supports_quota() && ! $this->is_action_to_same_source( $args ) ) {
1154 $quota_validation = $source->validate_quota( $bulk_args );
1155
1156 if ( is_wp_error( $quota_validation ) ) {
1157 return $quota_validation;
1158 }
1159
1160 if ( false === $quota_validation ) {
1161 return new \WP_Error( 'quota_error', 'The moving failed because it will pass the maximum templates you can save.' );
1162 }
1163 }
1164
1165 $bulk_save = $source->save_bulk_items( $bulk_args );
1166
1167 if ( ! empty( $bulk_save ) ) {
1168 $this->bulk_delete_templates( [
1169 'template_ids' => $args['from_template_id'],
1170 'source' => $args['from_source'],
1171 ] );
1172 }
1173
1174 return $bulk_save;
1175 }
1176
1177 public function bulk_copy_templates( array $args ) {
1178 $validate_args = $this->ensure_args( [ 'source', 'from_source', 'from_template_id' ], $args );
1179
1180 if ( is_wp_error( $validate_args ) ) {
1181 return $validate_args;
1182 }
1183
1184 $args['source'] = $args['source'][0];
1185
1186 return $this->bulk_copy_template_items( $args );
1187 }
1188
1189 private function bulk_copy_template_items( array $args ) {
1190 $source = $this->get_source( $args['source'] );
1191
1192 if ( ! $source ) {
1193 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
1194 }
1195
1196 $from_source = $this->get_source( $args['from_source'] );
1197
1198 if ( ! $from_source ) {
1199 return new \WP_Error( 'template_error', self::ERROR_TEMPLATE_SOURCE_NOT_FOUND );
1200 }
1201
1202 $bulk_args = $from_source->format_args_for_bulk_action( $args );
1203
1204 if ( $source->supports_quota() && ! $this->is_action_to_same_source( $args ) ) {
1205 $quota_validation = $source->validate_quota( $bulk_args );
1206
1207 if ( is_wp_error( $quota_validation ) ) {
1208 return $quota_validation;
1209 }
1210
1211 if ( false === $quota_validation ) {
1212 return new \WP_Error( 'quota_error', 'The copying failed because it will pass the maximum templates you can save.' );
1213 }
1214 }
1215
1216 return $source->save_bulk_items( $bulk_args );
1217 }
1218
1219 public function get_templates_quota( array $args ) {
1220 $validate_args = $this->ensure_args( [ 'source' ], $args );
1221
1222 if ( is_wp_error( $validate_args ) ) {
1223 return $validate_args;
1224 }
1225
1226 $source = $this->get_source( $args['source'] );
1227
1228 if ( ! $source ) {
1229 return new \WP_Error( 'template_error', 'Source not found.' );
1230 }
1231
1232 return $source->get_quota();
1233 }
1234 }
1235