PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.7.2
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.7.2
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
templately / includes / Builder / TemplateLoader.php

TemplateLoader.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.7.2, at includes/Builder/TemplateLoader.php

456 lines 17.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Builder;
4
5 use Elementor\Plugin;
6 use Templately\Builder\Managers\LocationManager;
7 use Templately\Builder\Managers\ThemeCompatibility;
8 use Templately\Builder\Types\BaseTemplate;
9 use Templately\Utils\Views;
10 use ElementorPro\Modules\ThemeBuilder\Module;
11
12 class TemplateLoader {
13 /**
14 * @var Views
15 */
16 private $views;
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 public function __construct( $builder, $views ) {
97 $this->views = $views;
98
99 // new ThemeCompatibility();
100
101 add_action( 'get_header', [ $this, 'get_header' ], 0 );
102 add_action( 'get_footer', [ $this, 'get_footer' ], 0 );
103 add_action( 'elementor/document/wrapper_attributes', [ $this, 'wrapper_attributes' ], 10, 2 );
104 add_action( 'template_redirect', array( $this, 'set_global_product' ) );
105
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 /**
109 * Only for Development Mode.
110 */
111 if ( defined( 'TEMPLATELY_DEV_VIEWS' ) && TEMPLATELY_DEV_VIEWS ) {
112 add_action( 'templately_builder_header_before', [ $this, 'header_helper' ], 0 );
113 add_action( 'templately_builder_footer_before', [ $this, 'footer_helper' ], 0 );
114 }
115 }
116
117 public function header_helper() {
118 echo '<small>Header</small>';
119 }
120
121 public function footer_helper() {
122 echo '<small>Footer</small>';
123 }
124
125 public function get_header() {
126 if(!self::is_header_footer('header')){
127 return;
128 }
129 $this->views->get_header();
130
131 $templates = [];
132 $templates[] = 'header.php';
133
134 remove_all_actions( 'wp_head' );
135
136 ob_start();
137 locate_template( $templates, true );
138 ob_get_clean();
139 }
140
141 public function get_footer( $name ) {
142 if(!self::is_header_footer('footer')){
143 return;
144 }
145 $this->views->get_footer();
146
147 $templates = [];
148 $name = (string) $name;
149 if ( '' !== $name ) {
150 $templates[] = "footer-{$name}.php";
151 }
152
153 $templates[] = 'footer.php';
154
155 // remove_all_actions( 'wp_footer' );
156
157 ob_start();
158 locate_template( $templates, true );
159 ob_get_clean();
160 }
161
162 public static function is_header_footer($type = null){
163 if(class_exists( 'Elementor\Plugin' )){
164 $pid = get_the_ID();
165 $post_type = get_post_type($pid);
166 $document = Plugin::$instance->documents->get( $pid );
167
168 if(
169 $post_type === 'elementor_library' &&
170 (
171 $document->get_type() === 'header' ||
172 $document->get_type() === 'footer')
173 ){
174 return false;
175 }
176 }
177
178 $header = templately()->theme_builder::$conditions_manager->get_templates_by_location( 'header' );
179 $footer = templately()->theme_builder::$conditions_manager->get_templates_by_location( 'footer' );
180
181 if($type === 'header'){
182 return $header;
183 }
184 else if($type === 'footer'){
185 return $footer;
186 }
187
188 if(!empty($header) || !empty($footer)){
189 return true;
190 }
191 return false;
192 }
193
194 public function wrapper_attributes( $attributes, $document ) {
195 $post_type = get_post_type($document->get_main_id());
196
197 if( $post_type === 'templately_library' ){
198 $template = templately()->theme_builder->get_template( $document->get_main_id() );
199 if($template){
200 $attributes['data-elementor-type'] = 'templately-' . $template->get_type();
201 $attributes['data-elementor-id'] = $template->get_main_id();
202 $attributes['data-elementor-post-type'] = 'templately_library';
203 $attributes['data-elementor-title'] = $template->get_title();
204 $attributes['class'] .= ' ' . implode( ' ', get_post_class() );
205 }
206 }
207
208 return $attributes;
209 }
210
211 public function set_global_product(){
212 global $product;
213
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() );
452 }
453 }
454 }
455
456 }