PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.3.3
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.3.3
3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 All 111 releases
← All changes | includes/Builder/TemplateLoader.php +4 -318 3.7.13.3.3 View file →
@@ -14,86 +14,8 @@
14 14 * @var Views
15 15 */
16 16 private $views;
17 17
18 - /**
19 - * Stack of booleans, one per in-flight document render, recording whether
20 - * that call switched the main query to a preview product. A stack (not a
21 - * single flag) so nested/re-entrant renders — e.g. a `product_single`
22 - * template embedding another Elementor document via a Template widget —
23 - * restore correctly. Static ( not per-instance ) because the hooks that
24 - * drive it are registered unconditionally ( register_preview_product_hooks() ),
25 - * independent of whether a per-request TemplateLoader instance exists.
26 - *
27 - * @var bool[]
28 - */
29 - private static $preview_query_switch_stack = [];
30 -
31 - /**
32 - * Registers the WooCommerce preview-product query switch, unconditionally
33 - * ( admin and front end alike — unlike the rest of this class, which is only
34 - * instantiated on the front-end `wp` hook ). Called once from ThemeBuilder's
35 - * constructor.
36 - *
37 - * Three independent render pathways need their own switch, because none of
38 - * them share a common choke point:
39 - * - Real front end / live preview iframe: Document::get_content() ->
40 - * Frontend::get_builder_content(). This is a normal front-end request, so
41 - * it WOULD also be reachable from a `wp`-gated instance, but is registered
42 - * here too for symmetry with the pathways below and so both share one stack.
43 - * - Editor admin page bootstrap ( post.php?action=elementor, which never
44 - * fires the `wp` action ): Document::get_config() bakes each widget's
45 - * server-rendered `render()` output into `ElementorConfig.initial_document`
46 - * as `htmlCache` via get_elements_raw_data( null, true ), fired right after
47 - * `before_get_config`. This does NOT go through Frontend::get_builder_content()
48 - * at all, so it needs its own switch/restore pair.
49 - * - Adding a widget to the LIVE canvas ( no save/reload in between ): a PHP-only
50 - * widget with no JS `content_template()` — every EA Woo widget, and most of
51 - * ours — is rendered via the `render_widget` ajax action
52 - * ( Widgets_Manager::ajax_render_widget() -> Document::render_element() ),
53 - * which goes through NEITHER get_content() NOR get_config(). Confirmed by
54 - * direct testing: dropping one of these widgets onto a brand-new, unsaved
55 - * product_single template shows the widget's placeholder until the whole
56 - * document is reloaded ( which bakes via get_config() and so IS covered ) —
57 - * reloading immediately after masks the gap, which is why it wasn't caught
58 - * sooner.
59 - *
60 - * Two other approaches were tried and confirmed NOT to work, by direct
61 - * testing with temporary logging (kept here so the next person doesn't
62 - * re-walk the same dead ends):
63 - * - Switching the query earlier ( e.g. on `wp_ajax_elementor_ajax` ), on the
64 - * theory that `ajax_render_widget()`'s own `query_posts( [ 'p' =>
65 - * $editor_post_id, 'post_type' => 'any' ] )` (widgets.php) would silently
66 - * undo it. Red herring: `query_posts()` only replaces `$wp_query` — it
67 - * does NOT touch `global $post` at all ( that only happens via
68 - * `the_post()` / `setup_postdata()`, neither of which `ajax_render_widget()`
69 - * calls on its query ) — confirmed via a `the_posts` filter showing the
70 - * query itself resolving to the right product while `$GLOBALS['post']`
71 - * stayed on the templately_library document throughout.
72 - * - Rewriting that same `query_posts()` query via `pre_get_posts`. The query
73 - * rewrite itself worked ( confirmed the SQL fetched the preview product
74 - * correctly ) but was moot for the same reason: since `query_posts()`
75 - * never touches `global $post`, redirecting it changes nothing observable.
76 - *
77 - * The actual cause: `Ajax\Module::handle_ajax_request()` calls
78 - * `Plugin::$instance->db->switch_to_post( $editor_post_id )` — which DOES
79 - * directly set `$GLOBALS['post']` + `setup_postdata()` — *before* dispatching
80 - * to any registered action (including `render_widget`), and nothing after
81 - * that point ever touches `global $post` again. So the fix has to override
82 - * `global $post` a second time, strictly *after* that call. `elementor/ajax/
83 - * register_actions` fires right after it and before the action dispatch loop
84 - * — see maybe_switch_global_post_for_ajax_render(). No restore needed: this
85 - * is a one-shot admin-ajax.php process that exits right after responding.
86 - */
87 - public static function register_preview_product_hooks() {
88 - add_action( 'elementor/frontend/before_get_builder_content', [ self::class, 'maybe_switch_to_preview_product_query' ] );
89 - add_filter( 'elementor/frontend/the_content', [ self::class, 'maybe_restore_preview_product_query' ] );
90 - add_action( 'elementor/document/before_get_config', [ self::class, 'maybe_switch_to_preview_product_query' ] );
91 - add_filter( 'elementor/document/config', [ self::class, 'maybe_restore_preview_product_query' ] );
92 -
93 - add_action( 'elementor/ajax/register_actions', [ self::class, 'maybe_switch_global_post_for_ajax_render' ] );
94 - }
95 -
96 18 public function __construct( $builder, $views ) {
97 19 $this->views = $views;
98 20
99 21 // new ThemeCompatibility();
@@ -102,10 +24,8 @@
102 24 add_action( 'get_footer', [ $this, 'get_footer' ], 0 );
103 25 add_action( 'elementor/document/wrapper_attributes', [ $this, 'wrapper_attributes' ], 10, 2 );
104 26 add_action( 'template_redirect', array( $this, 'set_global_product' ) );
105 27
106 - add_action( 'templately_builder_header_after', [ self::class, 'print_style_tags' ], 0 );
107 - add_action( 'templately_builder_footer_before', [ self::class, 'print_style_tags' ], 0 );
108 28 /**
109 29 * Only for Development Mode.
110 30 */
111 31 if ( defined( 'TEMPLATELY_DEV_VIEWS' ) && TEMPLATELY_DEV_VIEWS ) {
@@ -210,247 +130,13 @@
210 130
211 131 public function set_global_product(){
212 132 global $product;
213 133
214 - if ( ! function_exists( 'wc_setup_product_data' ) ) {
215 - return;
216 - }
217 -
218 - // A real product is already in context (e.g. a live single-product page) — never override it.
219 - if ( $product instanceof \WC_Product ) {
220 - return;
221 - }
222 -
223 - $document_id = get_the_ID();
224 - if ( ! $document_id ) {
225 - return;
226 - }
227 -
228 - // The current post is itself a product — set it up (prior behaviour, kept explicit).
229 - if ( get_post_type( $document_id ) === 'product' ) {
230 - wc_setup_product_data( $document_id );
231 - return;
232 - }
233 -
234 - // Otherwise only fabricate a preview product for a WooCommerce single-product
235 - // Theme Builder template being rendered in the editor / preview. This mirrors what
236 - // Elementor Pro's Single Product document does: point the global $product at a real
237 - // product so any WooCommerce widget — ours and third-party (e.g. EA Woo Product Tabs) —
238 - // renders real data in the editor instead of appearing empty. The live front end is
239 - // unaffected (there a real product is always in context, handled above).
240 - //
241 - // This only sets `global $product`; it does NOT cover widgets/plugins that re-derive
242 - // the product from `global $post` (e.g. via `wc_get_product( false )`'s fallback, which
243 - // checks `get_post_type( $post->ID )`) instead of trusting a pre-set `$product`. That
244 - // gap is closed separately by maybe_switch_to_preview_product_query() below.
245 - if ( ! self::is_woo_single_preview( $document_id ) ) {
246 - return;
247 - }
248 -
249 - $preview_product_id = self::get_preview_product_id();
250 - if ( $preview_product_id ) {
251 - wc_setup_product_data( $preview_product_id );
252 - }
253 - }
254 -
255 - /**
256 - * Swap the main query ( and `global $post` ) to the preview product for the
257 - * duration of a `product_single` Theme Builder document's content render.
258 - *
259 - * Mirrors Elementor Pro's `Theme_Document::get_content()` / `Preview_Manager`
260 - * ( `switch_to_preview_query()` + `DB::switch_to_query( $query_vars, true )` ),
261 - * but built only on Elementor core APIs so it works without Elementor Pro.
262 - * This is what makes widgets that re-derive the product from `global $post`
263 - * ( e.g. EA's Woo Product Description, or `wc_get_product( false )`'s own
264 - * fallback ) render real content instead of a placeholder — setting
265 - * `global $product` alone ( set_global_product() above ) isn't enough for them.
266 - *
267 - * Hooked to both `elementor/frontend/before_get_builder_content` (real front end /
268 - * live preview iframe) and `elementor/document/before_get_config` (editor admin
269 - * page bootstrap), paired respectively with maybe_restore_preview_product_query()
270 - * on `elementor/frontend/the_content` and `elementor/document/config`.
271 - * `DB::switch_to_query()` / `restore_current_query()` are themselves stack-based,
272 - * but the hook only gives us the document on the "before" call, so a small
273 - * boolean stack here is what lets the "after" filter know whether to restore.
274 - *
275 - * @param \Elementor\Core\Base\Document|null $document The document about to render.
276 - */
277 - public static function maybe_switch_to_preview_product_query( $document ) {
278 - $switched = false;
279 -
280 - if ( $document && function_exists( 'wc_setup_product_data' ) ) {
281 - $document_id = $document->get_main_id();
282 -
283 - if ( self::is_woo_single_preview( $document_id ) ) {
284 - $preview_product_id = self::get_preview_product_id();
285 -
286 - if ( $preview_product_id ) {
287 - Plugin::$instance->db->switch_to_query( [
288 - 'p' => $preview_product_id,
289 - 'post_type' => 'product',
290 - ], true );
291 -
292 - $switched = true;
293 - }
294 - }
295 - }
296 -
297 - self::$preview_query_switch_stack[] = $switched;
298 - }
299 -
300 - /**
301 - * Restore the query switched by maybe_switch_to_preview_product_query(), if any.
302 - *
303 - * @param string $content Unmodified — this is a restore point, not a content filter.
304 - * @return string
305 - */
306 - public static function maybe_restore_preview_product_query( $content ) {
307 - if ( array_pop( self::$preview_query_switch_stack ) ) {
308 - Plugin::$instance->db->restore_current_query();
309 - }
310 -
311 - return $content;
312 - }
313 -
314 - /**
315 - * Whether `$document_id` is a WooCommerce single-product ( `product_single` )
316 - * Theme Builder template at all — regardless of edit/preview mode.
317 - *
318 - * @param int $document_id The templately_library post being checked.
319 - * @return bool
320 - */
321 - private static function is_woo_single_document( $document_id ) {
322 - if ( get_post_meta( $document_id, Source::TYPE_META_KEY, true ) !== 'product_single' ) {
323 - return false;
324 - }
325 -
326 - return post_type_exists( 'product' );
327 - }
328 -
329 - /**
330 - * Whether the current request is the editor/preview render of a WooCommerce
331 - * single-product ( `product_single` ) Theme Builder template.
332 - *
333 - * @param int $document_id The templately_library post being rendered.
334 - * @return bool
335 - */
336 - private static function is_woo_single_preview( $document_id ) {
337 - if ( ! self::is_woo_single_document( $document_id ) ) {
338 - return false;
339 - }
340 -
341 - return Plugin::$instance->editor->is_edit_mode()
342 - || Plugin::$instance->preview->is_preview_mode( $document_id )
343 - || ( isset( $_GET['templately_library'], $_GET['preview_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
344 - }
345 -
346 - /**
347 - * Third preview-product switch point: overrides `global $post` for the
348 - * `render_widget` ajax action — used when a widget with no JS
349 - * `content_template()` is freshly added to the live editor canvas ( see
350 - * register_preview_product_hooks() for the two dead ends this replaced ).
351 - *
352 - * `Ajax\Module::handle_ajax_request()` calls `Plugin::$instance->db
353 - * ->switch_to_post( $editor_post_id )` — which sets `$GLOBALS['post']` +
354 - * `setup_postdata()` directly — before dispatching to any action, and nothing
355 - * in `ajax_render_widget()` ( including its own `query_posts()` call, which
356 - * never touches `global $post` ) touches it again afterwards. So this has to
357 - * override `global $post` a second time, the same way `switch_to_post()`
358 - * itself does, timed to run strictly after it: `elementor/ajax/register_actions`
359 - * fires right after that call and before the action-dispatch loop.
360 - *
361 - * Reads `$_REQUEST['actions']` — the same raw JSON `Ajax\Module` itself decodes
362 - * moments later — read-only, purely to detect whether a `render_widget` call is
363 - * queued in this batch; no nonce check needed for that inspection since nothing
364 - * is mutated based on it beyond which product post to point `global $post` at.
365 - *
366 - * No restore call: a single, short-lived admin-ajax.php process that exits
367 - * right after responding, so nothing persists past it.
368 - */
369 - public static function maybe_switch_global_post_for_ajax_render() {
370 - if ( ! function_exists( 'wc_setup_product_data' ) || empty( $_REQUEST['actions'] ) || empty( $_REQUEST['editor_post_id'] ) ) {
371 - return;
372 - }
373 -
374 - // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only inspection, see docblock above.
375 - $actions = json_decode( wp_unslash( $_REQUEST['actions'] ), true );
376 -
377 - if ( ! is_array( $actions ) ) {
378 - return;
379 - }
380 -
381 - $has_render_widget = false;
382 -
383 - foreach ( $actions as $action_data ) {
384 - if ( isset( $action_data['action'] ) && 'render_widget' === $action_data['action'] ) {
385 - $has_render_widget = true;
386 - break;
387 - }
388 - }
389 -
390 - if ( ! $has_render_widget ) {
391 - return;
392 - }
393 -
394 - $document_id = absint( $_REQUEST['editor_post_id'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
395 -
396 - if ( ! self::is_woo_single_document( $document_id ) ) {
397 - return;
398 - }
399 -
400 - $preview_product_id = self::get_preview_product_id();
401 -
402 - if ( ! $preview_product_id ) {
403 - return;
404 - }
405 -
406 - $GLOBALS['post'] = get_post( $preview_product_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
407 -
408 - setup_postdata( $GLOBALS['post'] );
409 - }
410 -
411 - /**
412 - * Resolve the newest published product to use as the editor preview product.
413 - * Matches the "latest product" approach used by the dynamic Theme Builder
414 - * widgets ( Post_Content / Post_Title / Featured_Image ) and Elementor Pro.
415 - *
416 - * @return int Product ID, or 0 when the store has no published products.
417 - */
418 - private static function get_preview_product_id() {
419 - $products = get_posts( [
420 - 'post_type' => 'product',
421 - 'post_status' => 'publish',
422 - 'numberposts' => 1,
423 - 'orderby' => 'date',
424 - 'order' => 'DESC',
425 - 'fields' => 'ids',
426 - 'no_found_rows' => true,
427 - 'suppress_filters' => false,
428 - ] );
429 -
430 - return ! empty( $products ) ? (int) $products[0] : 0;
431 - }
432 -
433 - /**
434 - * Print remaining enqueued styles with error handling.
435 - */
436 - public static function print_style_tags() {
437 - try {
438 - $wp_styles = wp_styles();
439 - // handles dependencies
440 - $wp_styles->do_items();
441 - if ( is_object( $wp_styles ) && is_array( $wp_styles->queue ?? null ) ) {
442 - foreach ( $wp_styles->queue as $style ) {
443 - if ( is_string( $style ) && ! $wp_styles->query( $style, 'done' ) ) {
444 - $wp_styles->do_item( $style );
445 - $wp_styles->done[] = $style;
446 - }
447 - }
448 - }
449 - } catch ( \Throwable $e ) {
450 - if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
451 - error_log( 'Templately: Error printing styles - ' . $e->getMessage() );
134 + if ( class_exists('\WC_Product') && ! $product instanceof \WC_Product ) {
135 + $product_id = get_the_ID();
136 + if ( $product_id ) {
137 + wc_setup_product_data( $product_id );
452 138 }
453 139 }
454 140 }
455 141
456 142 }