PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.0
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.0
1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 1.2.0 All 28 releases
xspeed / includes / class-builder-editor.php

class-builder-editor.php in xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN 1.3.0, at includes/class-builder-editor.php

180 lines 6.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Builder_Editor — "is this request a page-builder editing screen?"
4 *
5 * Page builders do not edit inside wp-admin. Beaver Builder, Elementor,
6 * Divi, Brizy, Oxygen and friends all render their editor over an ordinary
7 * FRONT-END URL, flagged by a query argument:
8 *
9 * /my-page/?fl_builder Beaver Builder
10 * /?p=12&elementor-preview=12 Elementor
11 * /my-page/?et_fb=1 Divi
12 *
13 * Every guard in this plugin is `is_admin() || DOING_AJAX || DOING_CRON ||
14 * REST_REQUEST`, and an editing screen is none of those. So the optimizer
15 * treats the editor exactly like a public page: combining its scripts,
16 * deferring them, stripping "bloat" it considers unnecessary.
17 *
18 * That breaks the editor outright. Combine JS merges ~21 builder handles
19 * into one dependency-free footer bundle, so `fl-builder.min.js` runs
20 * before `jquery.nanoscroller` and `fl-builder-system` exist — the toolbar
21 * and canvas never render and the console fills with `… is not a function`.
22 * The user cannot edit their page, and nothing in the UI points at us. (#281)
23 *
24 * There is no performance argument on the other side. An editing screen is
25 * a logged-in, single-user, uncacheable request; optimizing it trades a
26 * benefit nobody measures for a risk that costs the user their editor.
27 *
28 * @package XSpeed
29 */
30
31 declare(strict_types=1);
32
33 namespace XSpeed;
34
35 defined( 'ABSPATH' ) || exit;
36
37 /**
38 * Detects page-builder editing/preview requests on the front end.
39 *
40 * ## Why detection is deliberately conservative
41 *
42 * This predicate DISABLES optimization, so its failure modes are not
43 * symmetric:
44 *
45 * - a false negative breaks one builder's editor (the bug we already have)
46 * - a false positive silently disables optimization on real pages, which
47 * is invisible, site-wide, and reported as "xSpeed does nothing"
48 *
49 * So every signal below is a query argument a builder only ever sets on its
50 * own editing screen. We do NOT infer from `is_user_logged_in()` or
51 * `current_user_can( 'edit_posts' )` — an editor browsing their own site is
52 * a normal front-end visitor whose pages should still be optimized. (#203
53 * tracks that broader question separately.)
54 */
55 class Builder_Editor {
56
57 /**
58 * Query arguments that mean "a builder is editing this page".
59 *
60 * Presence alone is the signal — several builders set the argument with
61 * an empty value (`?fl_builder`), so a truthiness test would miss them.
62 *
63 * Keep this list additive. Removing an entry re-breaks a builder.
64 *
65 * @var string[]
66 */
67 private const EDITOR_QUERY_ARGS = array(
68 // Beaver Builder — the editor, and the iframe it renders its UI in.
69 'fl_builder',
70 'fl_builder_ui_iframe',
71 // Elementor — the preview iframe inside the editor.
72 'elementor-preview',
73 // Brizy.
74 'brizy-edit',
75 'brizy-edit-iframe',
76 // Divi — visual builder, and the backend (wireframe) builder.
77 'et_fb',
78 'et_bfb',
79 // Visual Composer — current and legacy argument names.
80 'vcv-editable',
81 'vcv-be-editor',
82 'vc_editable',
83 // Oxygen.
84 'ct_builder',
85 // SiteOrigin Page Builder live editor.
86 'siteorigin_panels_live_editor',
87 // Thrive Architect.
88 'tve',
89 // WPBakery frontend editor.
90 'vc_action',
91 );
92
93 /**
94 * Memoized result. The answer cannot change within a request, and this
95 * is consulted from several modules' boot paths.
96 *
97 * @var bool|null
98 */
99 private static $is_editor = null;
100
101 /**
102 * Is the current request a page-builder editing or preview screen?
103 */
104 public static function is_active(): bool {
105 if ( null !== self::$is_editor ) {
106 return self::$is_editor;
107 }
108
109 $found = self::detect();
110
111 /**
112 * Filter whether this request is a page-builder editing screen.
113 *
114 * Lets a site rescue a builder we do not know about — or force
115 * optimization back on for one we detect too eagerly — without
116 * patching the plugin.
117 *
118 * @param bool $found Whether a builder-editor signal was detected.
119 */
120 self::$is_editor = (bool) apply_filters( 'xspeed_is_builder_editor', $found );
121
122 return self::$is_editor;
123 }
124
125 /**
126 * The unfiltered detection itself.
127 */
128 private static function detect(): bool {
129 // Only a front-end request can be a builder editor. wp-admin, AJAX,
130 // cron and REST are already excluded by every caller's own guard;
131 // repeating it here keeps this correct when called from anywhere.
132 if ( is_admin()
133 || ( defined( 'DOING_AJAX' ) && DOING_AJAX )
134 || ( defined( 'DOING_CRON' ) && DOING_CRON )
135 ) {
136 return false;
137 }
138
139 foreach ( self::EDITOR_QUERY_ARGS as $arg ) {
140 // isset(), not a value check: `?fl_builder` carries no value.
141 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request-shape detection; no state is changed and no input is used beyond "is this key present".
142 if ( isset( $_GET[ $arg ] ) ) {
143 return true;
144 }
145 }
146
147 // Elementor's editor frame itself, which posts `action=elementor`.
148 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only request-shape detection; the value is compared, never used.
149 if ( isset( $_REQUEST['action'] ) && 'elementor' === $_REQUEST['action'] ) {
150 return true;
151 }
152
153 // Ask the builders that expose a runtime answer. These are more
154 // reliable than a query argument when available, and cover editor
155 // sub-requests that carry no argument of their own.
156 if ( class_exists( '\FLBuilderModel' ) && method_exists( '\FLBuilderModel', 'is_builder_active' ) && \FLBuilderModel::is_builder_active() ) {
157 return true;
158 }
159
160 if ( class_exists( '\Elementor\Plugin' ) ) {
161 $elementor = \Elementor\Plugin::$instance;
162 if ( isset( $elementor->preview ) && method_exists( $elementor->preview, 'is_preview_mode' ) && $elementor->preview->is_preview_mode() ) {
163 return true;
164 }
165 if ( isset( $elementor->editor ) && method_exists( $elementor->editor, 'is_edit_mode' ) && $elementor->editor->is_edit_mode() ) {
166 return true;
167 }
168 }
169
170 return false;
171 }
172
173 /**
174 * Drop the memo. Tests only — a single request never changes answer.
175 */
176 public static function reset(): void {
177 self::$is_editor = null;
178 }
179 }
180