PluginProbe
Elementor Website Builder – more than just a page builder / 4.2.0-dev2
Elementor Website Builder – more than just a page builder v4.2.0-dev2
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 / editor / editor.php

editor.php in Elementor Website Builder – more than just a page builder 4.2.0-dev2, at core/editor/editor.php

724 lines 18.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Core\Editor;
3
4 use Elementor\Core\Breakpoints\Manager as Breakpoints_Manager;
5 use Elementor\Core\Common\Modules\Ajax\Module;
6 use Elementor\Core\Debug\Loading_Inspection_Manager;
7 use Elementor\Core\Editor\Loader\Editor_Loader_Factory;
8 use Elementor\Core\Editor\Loader\Editor_Loader_Interface;
9 use Elementor\Core\Settings\Manager as SettingsManager;
10 use Elementor\Plugin;
11 use Elementor\TemplateLibrary\Source_Local;
12 use Elementor\Utils;
13 use Elementor\Core\Editor\Data;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Elementor editor.
21 *
22 * Elementor editor handler class is responsible for initializing Elementor
23 * editor and register all the actions needed to display the editor.
24 *
25 * @since 1.0.0
26 */
27 class Editor {
28
29 /**
30 * User capability required to access Elementor editor.
31 */
32 const EDITING_CAPABILITY = 'edit_posts';
33
34 /**
35 * Post ID.
36 *
37 * Holds the ID of the current post being edited.
38 *
39 * @since 1.0.0
40 * @access private
41 *
42 * @var int Post ID.
43 */
44 private $post_id;
45
46 /**
47 * Whether the edit mode is active.
48 *
49 * Used to determine whether we are in edit mode.
50 *
51 * @since 1.0.0
52 * @access private
53 *
54 * @var bool Whether the edit mode is active.
55 */
56 private $is_edit_mode;
57
58 /**
59 * @var Notice_Bar
60 */
61 public $notice_bar;
62
63 /**
64 * @var Promotion
65 */
66 public $promotion;
67
68 /**
69 * @var Editor_Loader_Interface
70 */
71 private $loader;
72
73 /**
74 * Init.
75 *
76 * Initialize Elementor editor. Registers all needed actions to run Elementor,
77 * removes conflicting actions etc.
78 *
79 * Fired by `admin_action_elementor` action.
80 *
81 * @since 1.0.0
82 * @access public
83 *
84 * @param bool $to_die Optional. Whether to die at the end. Default is `true`.
85 */
86 public function init( $to_die = true ) {
87 if ( empty( $_REQUEST['post'] ) ) {
88 return;
89 }
90
91 $this->set_post_id( absint( $_REQUEST['post'] ) );
92
93 if ( ! $this->is_edit_mode( $this->post_id ) ) {
94 return;
95 }
96
97 // BC: From 2.9.0, the editor shouldn't handle the global post / current document.
98 // Use requested id and not the global in order to avoid conflicts with plugins that changes the global post.
99 query_posts( [
100 'p' => $this->post_id,
101 'post_type' => get_post_type( $this->post_id ),
102 ] );
103
104 Plugin::$instance->db->switch_to_post( $this->post_id );
105
106 $document = Plugin::$instance->documents->get( $this->post_id );
107
108 Plugin::$instance->documents->switch_to_document( $document );
109
110 // Change mode to Builder
111 $document->set_is_built_with_elementor( true );
112
113 // End BC.
114
115 Loading_Inspection_Manager::instance()->register_inspections();
116
117 // Send MIME Type header like WP admin-header.
118 @header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) );
119
120 self::send_document_isolation_policy_header();
121
122 add_filter( 'show_admin_bar', '__return_false' );
123
124 // Remove all WordPress actions
125 remove_all_actions( 'wp_head' );
126 remove_all_actions( 'wp_print_styles' );
127 remove_all_actions( 'wp_print_head_scripts' );
128 remove_all_actions( 'wp_footer' );
129
130 // Handle `wp_head`
131 add_action( 'wp_head', 'wp_enqueue_scripts', 1 );
132 add_action( 'wp_head', 'wp_print_styles', 8 );
133 add_action( 'wp_head', 'wp_print_head_scripts', 9 );
134 add_action( 'wp_head', 'wp_site_icon' );
135 add_action( 'wp_head', [ $this, 'editor_head_trigger' ], 30 );
136
137 // Handle `wp_footer`
138 add_action( 'wp_footer', 'wp_print_footer_scripts', 20 );
139 add_action( 'wp_footer', 'wp_auth_check_html', 30 );
140 add_action( 'wp_footer', [ $this, 'wp_footer' ] );
141
142 // Handle `wp_enqueue_scripts`
143 remove_all_actions( 'wp_enqueue_scripts' );
144
145 // Also remove all scripts hooked into after_wp_tiny_mce.
146 remove_all_actions( 'after_wp_tiny_mce' );
147
148 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ], 999999 );
149 add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_styles' ], 999999 );
150
151 // Setup default heartbeat options
152 add_filter( 'heartbeat_settings', function( $settings ) {
153 $settings['interval'] = 15;
154 return $settings;
155 } );
156
157 // Tell to WP Cache plugins do not cache this request.
158 Utils::do_not_cache();
159
160 do_action( 'elementor/editor/init' );
161
162 $this->get_loader()->print_root_template();
163
164 // From the action it's an empty string, from tests its `false`
165 if ( false !== $to_die ) {
166 die;
167 }
168 }
169
170 /**
171 * Retrieve post ID.
172 *
173 * Get the ID of the current post.
174 *
175 * @since 1.8.0
176 * @access public
177 *
178 * @return int Post ID.
179 */
180 public function get_post_id() {
181 return $this->post_id;
182 }
183
184 /**
185 * Redirect to new URL.
186 *
187 * Used as a fallback function for the old URL structure of Elementor page
188 * edit URL.
189 *
190 * Fired by `template_redirect` action.
191 *
192 * @since 1.6.0
193 * @access public
194 */
195 public function redirect_to_new_url() {
196 if ( ! isset( $_GET['elementor'] ) ) {
197 return;
198 }
199
200 $document = Plugin::$instance->documents->get( get_the_ID() );
201
202 if ( ! $document ) {
203 wp_die( esc_html__( 'Document not found.', 'elementor' ) );
204 }
205
206 if ( ! $document->is_editable_by_current_user() || ! $document->is_built_with_elementor() ) {
207 return;
208 }
209
210 wp_safe_redirect( $document->get_edit_url() );
211 die;
212 }
213
214 /**
215 * Whether the edit mode is active.
216 *
217 * Used to determine whether we are in the edit mode.
218 *
219 * @since 1.0.0
220 * @access public
221 *
222 * @param int $post_id Optional. Post ID. Default is `null`, the current
223 * post ID.
224 *
225 * @return bool Whether the edit mode is active.
226 */
227 public function is_edit_mode( $post_id = null ) {
228 if ( null !== $this->is_edit_mode ) {
229 return $this->is_edit_mode;
230 }
231
232 if ( empty( $post_id ) ) {
233 $post_id = $this->post_id;
234 }
235
236 $document = Plugin::$instance->documents->get( $post_id );
237
238 if ( ! $document || ! $document->is_editable_by_current_user() ) {
239 return false;
240 }
241
242 /** @var Module ajax */
243 $ajax_data = Plugin::$instance->common->get_component( 'ajax' )->get_current_action_data();
244
245 if ( ! empty( $ajax_data ) && 'get_document_config' === $ajax_data['action'] ) {
246 return true;
247 }
248
249 // Ajax request as Editor mode
250 $actions = [
251 'elementor',
252
253 // Templates
254 'elementor_get_templates',
255 'elementor_save_template',
256 'elementor_get_template',
257 'elementor_delete_template',
258 'elementor_import_template',
259 'elementor_library_direct_actions',
260 ];
261
262 if ( isset( $_REQUEST['action'] ) && in_array( $_REQUEST['action'], $actions ) ) {
263 return true;
264 }
265
266 return false;
267 }
268
269 /**
270 * Lock post.
271 *
272 * Mark the post as currently being edited by the current user.
273 *
274 * @since 1.0.0
275 * @access public
276 *
277 * @param int $post_id The ID of the post being edited.
278 */
279 public function lock_post( $post_id ) {
280 if ( ! function_exists( 'wp_set_post_lock' ) ) {
281 require_once ABSPATH . 'wp-admin/includes/post.php';
282 }
283
284 wp_set_post_lock( $post_id );
285 }
286
287 /**
288 * Get locked user.
289 *
290 * Check what user is currently editing the post.
291 *
292 * @since 1.0.0
293 * @access public
294 *
295 * @param int $post_id The ID of the post being edited.
296 *
297 * @return \WP_User|false User information or false if the post is not locked.
298 */
299 public function get_locked_user( $post_id ) {
300 if ( ! function_exists( 'wp_check_post_lock' ) ) {
301 require_once ABSPATH . 'wp-admin/includes/post.php';
302 }
303
304 $locked_user = wp_check_post_lock( $post_id );
305 if ( ! $locked_user ) {
306 return false;
307 }
308
309 return get_user_by( 'id', $locked_user );
310 }
311
312 /**
313 * NOTICE: This method not in use, it's here for backward compatibility.
314 *
315 * Print Editor Template.
316 *
317 * Include the wrapper template of the editor.
318 *
319 * @since 2.2.0
320 * @access public
321 */
322 public function print_editor_template() {
323 include ELEMENTOR_PATH . 'includes/editor-templates/editor-wrapper.php';
324 }
325
326 /**
327 * Enqueue scripts.
328 *
329 * Registers all the editor scripts and enqueues them.
330 *
331 * @since 1.0.0
332 * @access public
333 */
334 public function enqueue_scripts() {
335 remove_action( 'wp_enqueue_scripts', [ $this, __FUNCTION__ ], 999999 );
336
337 global $wp_styles, $wp_scripts;
338
339 // Reset global variable
340 $wp_styles = new \WP_Styles(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
341 $wp_scripts = new \WP_Scripts(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
342
343 $this->get_loader()->register_scripts();
344
345 /**
346 * Before editor enqueue scripts.
347 *
348 * Fires before Elementor editor scripts are enqueued.
349 *
350 * @since 1.0.0
351 */
352 do_action( 'elementor/editor/before_enqueue_scripts' );
353
354 // Tweak for WP Admin menu icons
355 wp_print_styles( 'editor-buttons' );
356
357 $this->get_loader()->enqueue_scripts();
358
359 Plugin::$instance->controls_manager->enqueue_control_scripts();
360
361 /**
362 * After editor enqueue scripts.
363 *
364 * Fires after Elementor editor scripts are enqueued.
365 *
366 * @since 1.0.0
367 */
368 do_action( 'elementor/editor/after_enqueue_scripts' );
369 }
370
371 /**
372 * Enqueue styles.
373 *
374 * Registers all the editor styles and enqueues them.
375 *
376 * @since 1.0.0
377 * @access public
378 */
379 public function enqueue_styles() {
380 /**
381 * Before editor enqueue styles.
382 *
383 * Fires before Elementor editor styles are enqueued.
384 *
385 * @since 1.0.0
386 */
387 do_action( 'elementor/editor/before_enqueue_styles' );
388
389 $this->get_loader()->register_styles();
390 $this->get_loader()->enqueue_styles();
391
392 $this->enqueue_theme_ui_styles();
393
394 $breakpoints = Plugin::$instance->breakpoints->get_breakpoints();
395
396 // The two breakpoints under 'tablet' need to be checked for values.
397 if ( $breakpoints[ Breakpoints_Manager::BREAKPOINT_KEY_MOBILE ]->is_custom() || $breakpoints[ Breakpoints_Manager::BREAKPOINT_KEY_MOBILE_EXTRA ]->is_enabled() ) {
398 wp_add_inline_style(
399 'elementor-editor',
400 '.elementor-device-tablet #elementor-preview-responsive-wrapper { width: ' . Plugin::$instance->breakpoints->get_device_min_breakpoint( Breakpoints_Manager::BREAKPOINT_KEY_TABLET ) . 'px; }'
401 );
402 }
403
404 /**
405 * After editor enqueue styles.
406 *
407 * Fires after Elementor editor styles are enqueued.
408 *
409 * @since 1.0.0
410 */
411 do_action( 'elementor/editor/after_enqueue_styles' );
412 }
413
414 private function enqueue_theme_ui_styles() {
415 $ui_theme_selected = SettingsManager::get_settings_managers( 'editorPreferences' )->get_model()->get_settings( 'ui_theme' );
416
417 $ui_themes = [
418 'light',
419 'dark',
420 ];
421
422 if ( 'auto' === $ui_theme_selected || ! in_array( $ui_theme_selected, $ui_themes, true ) ) {
423 $ui_light_theme_media_queries = '(prefers-color-scheme: light)';
424 $ui_dark_theme_media_queries = '(prefers-color-scheme: dark)';
425 } else {
426 $ui_light_theme_media_queries = 'none';
427 $ui_dark_theme_media_queries = 'none';
428
429 if ( 'light' === $ui_theme_selected ) {
430 $ui_light_theme_media_queries = 'all';
431 } elseif ( 'dark' === $ui_theme_selected ) {
432 $ui_dark_theme_media_queries = 'all';
433 }
434 }
435
436 $this->enqueue_theme_ui( 'light', $ui_light_theme_media_queries );
437 $this->enqueue_theme_ui( 'dark', $ui_dark_theme_media_queries );
438 }
439
440 private function enqueue_theme_ui( $ui_theme, $ui_theme_media_queries = 'all' ) {
441 $suffix = Utils::is_script_debug() ? '' : '.min';
442
443 wp_enqueue_style(
444 'e-theme-ui-' . $ui_theme,
445 ELEMENTOR_ASSETS_URL . 'css/theme-' . $ui_theme . $suffix . '.css',
446 [],
447 ELEMENTOR_VERSION,
448 $ui_theme_media_queries
449 );
450 }
451
452 /**
453 * Editor head trigger.
454 *
455 * Fires the 'elementor/editor/wp_head' action in the head tag in Elementor
456 * editor.
457 *
458 * @since 1.0.0
459 * @access public
460 */
461 public function editor_head_trigger() {
462 /**
463 * Elementor editor head.
464 *
465 * Fires on Elementor editor head tag.
466 *
467 * Used to prints scripts or any other data in the head tag.
468 *
469 * @since 1.0.0
470 */
471 do_action( 'elementor/editor/wp_head' );
472 }
473
474 /**
475 * WP footer.
476 *
477 * Prints Elementor editor with all the editor templates, and render controls,
478 * widgets and content elements.
479 *
480 * Fired by `wp_footer` action.
481 *
482 * @since 1.0.0
483 * @access public
484 */
485 public function wp_footer() {
486 $plugin = Plugin::$instance;
487
488 $plugin->controls_manager->render_controls();
489 $plugin->widgets_manager->render_widgets_content();
490 $plugin->elements_manager->render_elements_content();
491
492 $plugin->dynamic_tags->print_templates();
493
494 $this->get_loader()->register_additional_templates();
495
496 /**
497 * Elementor editor footer.
498 *
499 * Fires on Elementor editor before closing the body tag.
500 *
501 * Used to prints scripts or any other HTML before closing the body tag.
502 *
503 * @since 1.0.0
504 */
505 do_action( 'elementor/editor/footer' );
506 }
507
508 /**
509 * Set edit mode.
510 *
511 * Used to update the edit mode.
512 *
513 * @since 1.0.0
514 * @access public
515 *
516 * @param bool $edit_mode Whether the edit mode is active.
517 */
518 public function set_edit_mode( $edit_mode ) {
519 $this->is_edit_mode = $edit_mode;
520 }
521
522 /**
523 * Editor constructor.
524 *
525 * Initializing Elementor editor and redirect from old URL structure of
526 * Elementor editor.
527 *
528 * @since 1.0.0
529 * @access public
530 */
531 public function __construct() {
532 Plugin::$instance->data_manager_v2->register_controller( new Data\Globals\Controller() );
533
534 $this->notice_bar = new Notice_Bar();
535 $this->promotion = new Promotion();
536
537 add_action( 'admin_action_elementor', [ $this, 'init' ] );
538 add_action( 'template_redirect', [ $this, 'redirect_to_new_url' ] );
539
540 // Handle autocomplete feature for URL control.
541 add_filter( 'wp_link_query_args', [ $this, 'filter_wp_link_query_args' ] );
542 add_filter( 'wp_link_query', [ $this, 'filter_wp_link_query' ] );
543
544 add_filter( 'replace_editor', [ $this, 'filter_replace_editor' ], 10, 2 );
545 }
546
547 /**
548 * Whether the Document-Isolation-Policy header should be sent on the
549 * Elementor editor screen and the editor preview iframe.
550 *
551 * DIP places the document in its own agent cluster, which is the prerequisite
552 * for cross-origin isolation features such as SharedArrayBuffer (required by
553 * WordPress core's client-side media processing introduced in WP 7.1).
554 *
555 * Both the editor parent document and the preview iframe must send the same
556 * DIP header so they join the same agent cluster and synchronous DOM access
557 * between them (e.g. `iframe.contentWindow.elementorFrontend`) keeps working.
558 *
559 * The header is only honored by browsers on a secure context (HTTPS or
560 * localhost) so the helper short-circuits on insecure origins to avoid
561 * sending a header that the browser will ignore.
562 *
563 * @since 4.1.0
564 *
565 * @return bool
566 */
567 public static function should_use_document_isolation_policy() {
568 if ( ! is_ssl() ) {
569 $raw_host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
570 $host = strtolower( (string) strtok( $raw_host, ':' ) );
571
572 if ( 'localhost' !== $host && ! str_ends_with( $host, '.localhost' ) ) {
573 return false;
574 }
575 }
576
577 /**
578 * Filters whether Elementor sends the Document-Isolation-Policy header
579 * on the editor screen and preview iframe.
580 *
581 * @since 4.1.0
582 *
583 * @param bool $enabled Whether DIP is enabled. Defaults to true on a secure context.
584 */
585 return (bool) apply_filters( 'elementor/editor/use_document_isolation_policy', true );
586 }
587
588 /**
589 * Send the Document-Isolation-Policy header for the current response.
590 *
591 * Safe to call from both the Elementor editor screen handler and the
592 * preview iframe handler. No-op when {@see self::should_use_document_isolation_policy()}
593 * returns false.
594 *
595 * @since 4.1.0
596 */
597 public static function send_document_isolation_policy_header() {
598 if ( ! self::should_use_document_isolation_policy() || headers_sent() ) {
599 return;
600 }
601
602 header( 'Document-Isolation-Policy: isolate-and-credentialless' );
603 }
604
605 /**
606 * Signals to WordPress that Elementor is replacing the block editor on its own editor page,
607 * so that block-editor-specific behaviour (e.g. WP 7.0 COOP/COEP isolation headers) is not
608 * applied when the Elementor editor is active.
609 *
610 * @param bool $replace Whether the editor is being replaced.
611 * @param \WP_Post $post The post being edited.
612 *
613 * @return bool
614 */
615 public function filter_replace_editor( $replace, $post ) {
616 if ( isset( $_REQUEST['action'] ) && 'elementor' === $_REQUEST['action'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
617 return true;
618 }
619
620 return $replace;
621 }
622
623 /**
624 * @since 2.2.0
625 * @access public
626 */
627 public function filter_wp_link_query_args( $query ) {
628 $library_cpt_key = array_search( Source_Local::CPT, $query['post_type'], true );
629 if ( false !== $library_cpt_key ) {
630 unset( $query['post_type'][ $library_cpt_key ] );
631 }
632
633 return $query;
634 }
635
636 /**
637 * @since 2.2.0
638 * @access public
639 */
640 public function filter_wp_link_query( $results ) {
641
642 // PHPCS - The user data is not used.
643 if ( isset( $_POST['editor'] ) && 'elementor' === $_POST['editor'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
644 $post_type_object = get_post_type_object( 'post' );
645 $post_label = $post_type_object->labels->singular_name;
646
647 foreach ( $results as & $result ) {
648 if ( 'post' === get_post_type( $result['ID'] ) ) {
649 $result['info'] = $post_label;
650 }
651 }
652 }
653
654 return $results;
655 }
656
657 public function set_post_id( $post_id ) {
658 $this->post_id = $post_id;
659 }
660
661 /**
662 * Get loader.
663 *
664 * @return Editor_Loader_Interface
665 */
666 private function get_loader() {
667 if ( ! $this->loader ) {
668 $this->loader = Editor_Loader_Factory::create();
669
670 $this->loader->init();
671 }
672
673 return $this->loader;
674 }
675
676 /**
677 * Get elements presets.
678 *
679 * @return array
680 */
681 public function get_elements_presets() {
682 $element_types = Plugin::$instance->elements_manager->get_element_types();
683 $presets = [];
684
685 foreach ( $element_types as $el_type => $element ) {
686 $this->check_element_for_presets( $element, $el_type, $presets );
687 }
688
689 return $presets;
690 }
691
692 /**
693 * @return void
694 */
695 private function check_element_for_presets( $element, $el_type, &$presets ) {
696 $element_presets = $element->get_panel_presets();
697
698 if ( empty( $element_presets ) ) {
699 return;
700 }
701
702 foreach ( $element_presets as $key => $preset ) {
703 $this->maybe_add_preset( $el_type, $preset, $key, $presets );
704 }
705 }
706
707 /**
708 * @return void
709 */
710 private function maybe_add_preset( $el_type, $preset, $key, &$presets ) {
711 if ( $this->is_valid_preset( $el_type, $preset ) ) {
712 $presets[ $key ] = $preset;
713 }
714 }
715
716 /**
717 * @return boolean
718 */
719 private function is_valid_preset( $el_type, $preset ) {
720 return isset( $preset['replacements']['custom']['originalWidget'] )
721 && $el_type === $preset['replacements']['custom']['originalWidget'];
722 }
723 }
724