PluginProbe
Elementor Website Builder – more than just a page builder / 4.1.0
Elementor Website Builder – more than just a page builder v4.1.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 / core / documents-manager.php

documents-manager.php in Elementor Website Builder – more than just a page builder 4.1.0, at core/documents-manager.php

822 lines 20.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Core;
3
4 use Elementor\Core\Base\Document;
5 use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
6 use Elementor\Core\DocumentTypes\Page;
7 use Elementor\Core\DocumentTypes\Post;
8 use Elementor\Plugin;
9 use Elementor\TemplateLibrary\Source_Local;
10 use Elementor\Utils;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly.
14 }
15
16 /**
17 * Elementor documents manager.
18 *
19 * Elementor documents manager handler class is responsible for registering and
20 * managing Elementor documents.
21 *
22 * @since 2.0.0
23 */
24 class Documents_Manager {
25
26 /**
27 * Registered types.
28 *
29 * Holds the list of all the registered types.
30 *
31 * @since 2.0.0
32 * @access protected
33 *
34 * @var Document[]
35 */
36 protected $types = [];
37
38 /**
39 * Registered documents.
40 *
41 * Holds the list of all the registered documents.
42 *
43 * @since 2.0.0
44 * @access protected
45 *
46 * @var Document[]
47 */
48 protected $documents = [];
49
50 /**
51 * Current document.
52 *
53 * Holds the current document.
54 *
55 * @since 2.0.0
56 * @access protected
57 *
58 * @var Document
59 */
60 protected $current_doc;
61
62 /**
63 * Switched data.
64 *
65 * Holds the current document when changing to the requested post.
66 *
67 * @since 2.0.0
68 * @access protected
69 *
70 * @var array
71 */
72 protected $switched_data = [];
73
74 protected $cpt = [];
75
76 /**
77 * Documents manager constructor.
78 *
79 * Initializing the Elementor documents manager.
80 *
81 * @since 2.0.0
82 * @access public
83 */
84 public function __construct() {
85 add_action( 'elementor/documents/register', [ $this, 'register_default_types' ], 0 );
86 add_action( 'elementor/ajax/register_actions', [ $this, 'register_ajax_actions' ] );
87 add_filter( 'post_row_actions', [ $this, 'filter_post_row_actions' ], 11, 2 );
88 add_filter( 'page_row_actions', [ $this, 'filter_post_row_actions' ], 11, 2 );
89 add_filter( 'user_has_cap', [ $this, 'remove_user_edit_cap' ], 10, 3 );
90 add_filter( 'elementor/editor/localize_settings', [ $this, 'localize_settings' ] );
91 add_action( 'rest_api_init', [ $this, 'register_rest_routes' ] );
92 }
93
94 /**
95 * Register ajax actions.
96 *
97 * Process ajax action handles when saving data and discarding changes.
98 *
99 * Fired by `elementor/ajax/register_actions` action.
100 *
101 * @since 2.0.0
102 * @access public
103 *
104 * @param Ajax $ajax_manager An instance of the ajax manager.
105 */
106 public function register_ajax_actions( $ajax_manager ) {
107 $ajax_manager->register_ajax_action( 'save_builder', [ $this, 'ajax_save' ] );
108 $ajax_manager->register_ajax_action( 'discard_changes', [ $this, 'ajax_discard_changes' ] );
109 $ajax_manager->register_ajax_action( 'get_document_config', [ $this, 'ajax_get_document_config' ] );
110 }
111
112 /**
113 * Register default types.
114 *
115 * Registers the default document types.
116 *
117 * @since 2.0.0
118 * @access public
119 */
120 public function register_default_types() {
121 $default_types = [
122 'post' => Post::get_class_full_name(), // BC.
123 'wp-post' => Post::get_class_full_name(),
124 'wp-page' => Page::get_class_full_name(),
125 ];
126
127 foreach ( $default_types as $type => $class ) {
128 $this->register_document_type( $type, $class );
129 }
130 }
131
132 /**
133 * Register document type.
134 *
135 * Registers a single document.
136 *
137 * @since 2.0.0
138 * @access public
139 *
140 * @param string $type Document type name.
141 * @param string $class_name The name of the class that registers the document type.
142 * Full name with the namespace.
143 *
144 * @return Documents_Manager The updated document manager instance.
145 */
146 public function register_document_type( $type, $class_name ) {
147 $this->types[ $type ] = $class_name;
148
149 $cpt = $class_name::get_property( 'cpt' );
150
151 if ( $cpt ) {
152 foreach ( $cpt as $post_type ) {
153 $this->cpt[ $post_type ] = $type;
154 }
155 }
156
157 if ( $class_name::get_property( 'register_type' ) ) {
158 Source_Local::add_template_type( $type );
159 }
160
161 return $this;
162 }
163
164 /**
165 * Get document.
166 *
167 * Retrieve the document data based on a post ID.
168 *
169 * @since 2.0.0
170 * @access public
171 *
172 * @param int $post_id Post ID.
173 * @param bool $from_cache Optional. Whether to retrieve cached data. Default is true.
174 *
175 * @return false|Document Document data or false if post ID was not entered.
176 */
177 public function get( $post_id, $from_cache = true ) {
178 $this->register_types();
179
180 $post_id = absint( $post_id );
181
182 if ( ! $post_id || ! get_post( $post_id ) ) {
183 return false;
184 }
185
186 /**
187 * Retrieve document post ID.
188 *
189 * Filters the document post ID.
190 *
191 * @since 2.0.7
192 *
193 * @param int $post_id The post ID of the document.
194 */
195 $post_id = apply_filters( 'elementor/documents/get/post_id', $post_id );
196
197 if ( ! $from_cache || ! isset( $this->documents[ $post_id ] ) ) {
198 $doc_type = $this->get_doc_type_by_id( $post_id );
199 $doc_type_class = $this->get_document_type( $doc_type );
200
201 $this->documents[ $post_id ] = new $doc_type_class( [
202 'post_id' => $post_id,
203 ] );
204 }
205
206 return $this->documents[ $post_id ];
207 }
208
209 /**
210 * Retrieve a document after checking it exist and allowed to edit.
211 *
212 * @param string $id
213 * @return Document
214 * @throws \Exception If the document is not found or the current user is not allowed to edit it.
215 * @since 3.13.0
216 */
217 public function get_with_permissions( $id ): Document {
218 $document = $this->get( $id );
219
220 if ( ! $document ) {
221 throw new \Exception( 'Not found.' );
222 }
223
224 if ( ! $document->is_editable_by_current_user() ) {
225 throw new \Exception( 'Access denied.' );
226 }
227
228 return $document;
229 }
230
231 /**
232 * A `void` version for `get_with_permissions`.
233 *
234 * @param string $id
235 * @return void
236 * @throws \Exception If the document is not found or the current user is not allowed to edit it.
237 */
238 public function check_permissions( $id ) {
239 $this->get_with_permissions( $id );
240 }
241
242 /**
243 * Get document or autosave.
244 *
245 * Retrieve either the document or the autosave.
246 *
247 * @since 2.0.0
248 * @access public
249 *
250 * @param int $id Optional. Post ID. Default is `0`.
251 * @param int $user_id Optional. User ID. Default is `0`.
252 *
253 * @return false|Document The document if it exist, False otherwise.
254 */
255 public function get_doc_or_auto_save( $id, $user_id = 0 ) {
256 $document = $this->get( $id );
257 if ( $document && $document->get_autosave_id( $user_id ) ) {
258 $document = $document->get_autosave( $user_id );
259 }
260
261 return $document;
262 }
263
264 /**
265 * Get document for frontend.
266 *
267 * Retrieve the document for frontend use.
268 *
269 * @since 2.0.0
270 * @access public
271 *
272 * @param int $post_id Optional. Post ID. Default is `0`.
273 *
274 * @return false|Document The document if it exist, False otherwise.
275 */
276 public function get_doc_for_frontend( $post_id ) {
277 $preview_id = (int) Utils::get_super_global_value( $_GET, 'preview_id' );
278 $is_preview = is_preview();
279 $is_nonce_verify = wp_verify_nonce( Utils::get_super_global_value( $_GET, 'preview_nonce' ), 'post_preview_' . $preview_id );
280
281 if ( ( $is_preview && $is_nonce_verify ) || Plugin::$instance->preview->is_preview_mode() ) {
282 $document = $this->get_doc_or_auto_save( $post_id, get_current_user_id() );
283 } else {
284 $document = $this->get( $post_id );
285 }
286
287 return $document;
288 }
289
290 /**
291 * Get document type.
292 *
293 * Retrieve the type of any given document.
294 *
295 * @since 2.0.0
296 * @access public
297 *
298 * @param string $type
299 *
300 * @param string $fallback
301 *
302 * @return Document|bool The type of the document.
303 */
304 public function get_document_type( $type, $fallback = 'post' ) {
305 $types = $this->get_document_types();
306
307 if ( isset( $types[ $type ] ) ) {
308 return $types[ $type ];
309 }
310
311 if ( isset( $types[ $fallback ] ) ) {
312 return $types[ $fallback ];
313 }
314
315 return false;
316 }
317
318 /**
319 * Get document types.
320 *
321 * Retrieve the all the registered document types.
322 *
323 * @since 2.0.0
324 * @access public
325 *
326 * @param array $args Optional. An array of key => value arguments to match against
327 * the properties. Default is empty array.
328 * @param string $operator Optional. The logical operation to perform. 'or' means only one
329 * element from the array needs to match; 'and' means all elements
330 * must match; 'not' means no elements may match. Default 'and'.
331 *
332 * @return Document[] All the registered document types.
333 */
334 public function get_document_types( $args = [], $operator = 'and' ) {
335 $this->register_types();
336
337 if ( ! empty( $args ) ) {
338 $types_properties = $this->get_types_properties();
339
340 $filtered = wp_filter_object_list( $types_properties, $args, $operator );
341
342 return array_intersect_key( $this->types, $filtered );
343 }
344
345 return $this->types;
346 }
347
348 /**
349 * Get document types with their properties.
350 *
351 * @return array A list of properties arrays indexed by the type.
352 */
353 public function get_types_properties() {
354 $types_properties = [];
355
356 foreach ( $this->get_document_types() as $type => $class ) {
357 $types_properties[ $type ] = $class::get_properties();
358 }
359 return $types_properties;
360 }
361
362 /**
363 * Create a document.
364 *
365 * Create a new document using any given parameters.
366 *
367 * @since 2.0.0
368 * @access public
369 *
370 * @param string $type Document type.
371 * @param array $post_data An array containing the post data.
372 * @param array $meta_data An array containing the post meta data.
373 *
374 * @return Document The type of the document.
375 */
376 public function create( $type, $post_data = [], $meta_data = [] ) {
377 $class = $this->get_document_type( $type, false );
378
379 if ( ! $class ) {
380 return new \WP_Error( 500, sprintf( 'Type %s does not exist.', $type ) );
381 }
382
383 if ( empty( $post_data['post_title'] ) ) {
384 $post_data['post_title'] = esc_html__( 'Elementor', 'elementor' );
385 if ( 'post' !== $type ) {
386 $post_data['post_title'] = sprintf(
387 /* translators: %s: Document title. */
388 __( 'Elementor %s', 'elementor' ),
389 call_user_func( [ $class, 'get_title' ] )
390 );
391 }
392 $update_title = true;
393 }
394
395 $meta_data['_elementor_edit_mode'] = 'builder';
396
397 // Save the type as-is for plugins that hooked at `wp_insert_post`.
398 $meta_data[ Document::TYPE_META_KEY ] = $type;
399
400 $post_data['meta_input'] = $meta_data;
401
402 $post_types = $class::get_property( 'cpt' );
403
404 if ( ! empty( $post_types[0] ) && empty( $post_data['post_type'] ) ) {
405 $post_data['post_type'] = $post_types[0];
406 }
407
408 $post_id = wp_insert_post( $post_data );
409
410 if ( ! empty( $update_title ) ) {
411 $post_data['ID'] = $post_id;
412 $post_data['post_title'] .= ' #' . $post_id;
413
414 // The meta doesn't need update.
415 unset( $post_data['meta_input'] );
416
417 wp_update_post( $post_data );
418 }
419
420 /** @var Document $document */
421 $document = new $class( [
422 'post_id' => $post_id,
423 ] );
424
425 // Let the $document to re-save the template type by his way + version.
426 $document->save( [] );
427
428 return $document;
429 }
430
431 /**
432 * Remove user edit capabilities if document is not editable.
433 *
434 * Filters the user capabilities to disable editing in admin.
435 *
436 * @param array $allcaps An array of all the user's capabilities.
437 * @param array $caps Actual capabilities for meta capability.
438 * @param array $args Optional parameters passed to has_cap(), typically object ID.
439 *
440 * @return array
441 */
442 public function remove_user_edit_cap( $allcaps, $caps, $args ) {
443 global $pagenow;
444
445 if ( ! in_array( $pagenow, [ 'post.php', 'edit.php' ], true ) ) {
446 return $allcaps;
447 }
448
449 // Don't touch not existing or not allowed caps.
450 if ( empty( $caps[0] ) || empty( $allcaps[ $caps[0] ] ) ) {
451 return $allcaps;
452 }
453
454 $capability = $args[0];
455
456 if ( 'edit_post' !== $capability ) {
457 return $allcaps;
458 }
459
460 if ( empty( $args[2] ) ) {
461 return $allcaps;
462 }
463
464 $post_id = $args[2];
465
466 $document = Plugin::$instance->documents->get( $post_id );
467
468 if ( ! $document ) {
469 return $allcaps;
470 }
471
472 $allcaps[ $caps[0] ] = $document::get_property( 'is_editable' );
473
474 return $allcaps;
475 }
476
477 /**
478 * Filter Post Row Actions.
479 *
480 * Let the Document to filter the array of row action links on the Posts list table.
481 *
482 * @param array $actions
483 * @param \WP_Post $post
484 *
485 * @return array
486 */
487 public function filter_post_row_actions( $actions, $post ) {
488 $document = $this->get( $post->ID );
489
490 if ( $document ) {
491 $actions = $document->filter_admin_row_actions( $actions );
492 }
493
494 return $actions;
495 }
496
497 /**
498 * Save document data using ajax.
499 *
500 * Save the document on the builder using ajax, when saving the changes, and refresh the editor.
501 *
502 * @since 2.0.0
503 * @access public
504 *
505 * @param array $request Post ID.
506 *
507 * @throws \Exception If current user don't have permissions to edit the post or the post is not using Elementor.
508 *
509 * @return array The document data after saving.
510 */
511 public function ajax_save( $request ) {
512 $document = $this->get( $request['editor_post_id'] );
513
514 if ( ! $document->is_built_with_elementor() || ! $document->is_editable_by_current_user() ) {
515 throw new \Exception( 'Access denied.' );
516 }
517
518 $this->switch_to_document( $document );
519
520 // Set the post as global post.
521 Plugin::$instance->db->switch_to_post( $document->get_post()->ID );
522
523 $status = Document::STATUS_DRAFT;
524
525 if ( isset( $request['status'] ) && in_array( $request['status'], [ Document::STATUS_PUBLISH, Document::STATUS_PRIVATE, Document::STATUS_PENDING, Document::STATUS_AUTOSAVE ], true ) ) {
526 $status = $request['status'];
527 }
528
529 if ( Document::STATUS_AUTOSAVE === $status ) {
530 // If the post is a draft - save the `autosave` to the original draft.
531 // Allow a revision only if the original post is already published.
532 if ( in_array( $document->get_post()->post_status, [ Document::STATUS_PUBLISH, Document::STATUS_PRIVATE ], true ) ) {
533 $document = $document->get_autosave( 0, true );
534 }
535 }
536
537 // Set default page template because the footer-saver doesn't send default values,
538 // But if the template was changed from canvas to default - it needed to save.
539 if ( Utils::is_cpt_custom_templates_supported() && ! isset( $request['settings']['template'] ) ) {
540 $request['settings']['template'] = 'default';
541 }
542
543 $data = [
544 'elements' => $request['elements'],
545 'settings' => $request['settings'],
546 ];
547
548 $document->save( $data );
549
550 $post = $document->get_post();
551 $main_post = $document->get_main_post();
552
553 // Refresh after save.
554 $document = $this->get( $post->ID, false );
555
556 $return_data = [
557 'status' => $post->post_status,
558 'config' => [
559 'document' => [
560 'last_edited' => $document->get_last_edited(),
561 'urls' => [
562 'wp_preview' => $document->get_wp_preview_url(),
563 ],
564 ],
565 ],
566 ];
567
568 $post_status_object = get_post_status_object( $main_post->post_status );
569
570 if ( $post_status_object ) {
571 $return_data['config']['document']['status'] = [
572 'value' => $post_status_object->name,
573 'label' => $post_status_object->label,
574 ];
575 }
576
577 /**
578 * Returned documents ajax saved data.
579 *
580 * Filters the ajax data returned when saving the post on the builder.
581 *
582 * @since 2.0.0
583 *
584 * @param array $return_data The returned data.
585 * @param Document $document The document instance.
586 */
587 $return_data = apply_filters( 'elementor/documents/ajax_save/return_data', $return_data, $document );
588
589 return $return_data;
590 }
591
592 /**
593 * Ajax discard changes.
594 *
595 * Load the document data from an autosave, deleting unsaved changes.
596 *
597 * @param array $request
598 *
599 * @return bool True if changes discarded, False otherwise.
600 * @throws \Exception If current user don't have permissions to edit the post or the post is not using Elementor.
601 *
602 * @since 2.0.0
603 * @access public
604 */
605 public function ajax_discard_changes( $request ) {
606 $document = $this->get_with_permissions( $request['editor_post_id'] );
607
608 $autosave = $document->get_autosave();
609
610 if ( $autosave ) {
611 $success = $autosave->delete();
612 } else {
613 $success = true;
614 }
615
616 return $success;
617 }
618
619 public function ajax_get_document_config( $request ) {
620 $post_id = absint( $request['id'] );
621
622 Plugin::$instance->editor->set_post_id( $post_id );
623
624 $document = $this->get_doc_or_auto_save( $post_id );
625
626 if ( ! $document ) {
627 throw new \Exception( 'Not found.' );
628 }
629
630 if ( ! $document->is_editable_by_current_user() ) {
631 throw new \Exception( 'Access denied.' );
632 }
633
634 // Set the global data like $post, $authordata and etc
635 Plugin::$instance->db->switch_to_post( $post_id );
636
637 $this->switch_to_document( $document );
638
639 // Change mode to Builder
640 $document->set_is_built_with_elementor( true );
641
642 $doc_config = $document->get_config();
643
644 return $doc_config;
645 }
646
647 /**
648 * Switch to document.
649 *
650 * Change the document to any new given document type.
651 *
652 * @since 2.0.0
653 * @access public
654 *
655 * @param Document $document The document to switch to.
656 */
657 public function switch_to_document( $document ) {
658 // If is already switched, or is the same post, return.
659 if ( $this->current_doc === $document ) {
660 $this->switched_data[] = false;
661 return;
662 }
663
664 $this->switched_data[] = [
665 'switched_doc' => $document,
666 'original_doc' => $this->current_doc, // Note, it can be null if the global isn't set
667 ];
668
669 $this->current_doc = $document;
670 }
671
672 /**
673 * Restore document.
674 *
675 * Rollback to the original document.
676 *
677 * @since 2.0.0
678 * @access public
679 */
680 public function restore_document() {
681 $data = array_pop( $this->switched_data );
682
683 // If not switched, return.
684 if ( ! $data ) {
685 return;
686 }
687
688 $this->current_doc = $data['original_doc'];
689 }
690
691 /**
692 * Get current document.
693 *
694 * Retrieve the current document.
695 *
696 * @since 2.0.0
697 * @access public
698 *
699 * @return Document The current document.
700 */
701 public function get_current() {
702 return $this->current_doc;
703 }
704
705 public function localize_settings( $settings ) {
706 $translations = [];
707
708 foreach ( $this->get_document_types() as $type => $class ) {
709 $translations[ $type ] = $class::get_title();
710 }
711
712 return array_replace_recursive( $settings, [
713 'i18n' => $translations,
714 ] );
715 }
716
717 private function register_types() {
718 if ( ! did_action( 'elementor/documents/register' ) ) {
719 /**
720 * Register Elementor documents.
721 *
722 * @since 2.0.0
723 *
724 * @param Documents_Manager $this The document manager instance.
725 */
726 do_action( 'elementor/documents/register', $this );
727 }
728 }
729
730 /**
731 * Get create new post URL.
732 *
733 * Retrieve a custom URL for creating a new post/page using Elementor.
734 *
735 * @param string $post_type Optional. Post type slug. Default is 'page'.
736 * @param string|null $template_type Optional. Query arg 'template_type'. Default is null.
737 *
738 * @return string A URL for creating new post using Elementor.
739 */
740 public static function get_create_new_post_url( $post_type = 'page', $template_type = null ) {
741 $query_args = [
742 'action' => 'elementor_new_post',
743 'post_type' => $post_type,
744 ];
745
746 if ( $template_type ) {
747 $query_args['template_type'] = $template_type;
748 }
749
750 $new_post_url = add_query_arg( $query_args, admin_url( 'edit.php' ) );
751
752 $new_post_url = add_query_arg( '_wpnonce', wp_create_nonce( 'elementor_action_new_post' ), $new_post_url );
753
754 return $new_post_url;
755 }
756
757 private function get_doc_type_by_id( $post_id ) {
758 // Auto-save inherits from the original post.
759 if ( wp_is_post_autosave( $post_id ) ) {
760 $post_id = wp_get_post_parent_id( $post_id );
761 }
762
763 // Content built with Elementor.
764 $template_type = get_post_meta( $post_id, Document::TYPE_META_KEY, true );
765
766 if ( $template_type && isset( $this->types[ $template_type ] ) ) {
767 return $template_type;
768 }
769
770 // Elementor installation on a site with existing content (which doesn't contain Elementor's meta).
771 $post_type = get_post_type( $post_id );
772
773 return $this->cpt[ $post_type ] ?? 'post';
774 }
775
776 public function register_rest_routes() {
777 register_rest_route('elementor/v1/documents', '/(?P<id>\d+)/media/import', [
778 'methods' => \WP_REST_Server::CREATABLE,
779 'callback' => function( $request ) {
780 $post_id = $request->get_param( 'id' );
781
782 try {
783 $document = $this->get_with_permissions( $post_id );
784
785 $elements_data = $document->get_elements_data();
786
787 $import_data = $document->get_import_data( [
788 'content' => $elements_data,
789 ] );
790
791 $document->save( [
792 'elements' => $import_data['content'],
793 ] );
794
795 return new \WP_REST_Response( [
796 'success' => true,
797 'document_saved' => true,
798 ], 200 );
799
800 } catch ( \Exception $e ) {
801 return new \WP_Error(
802 'elementor_import_error',
803 $e->getMessage(),
804 [ 'status' => 500 ]
805 );
806 }
807 },
808 'permission_callback' => function() {
809 return current_user_can( 'manage_options' );
810 },
811 'args' => [
812 'id' => [
813 'required' => true,
814 'validate_callback' => function( $param ) {
815 return is_numeric( $param );
816 },
817 ],
818 ],
819 ]);
820 }
821 }
822