PluginProbe ʕ •ᴥ•ʔ
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More / 4.0.7
Superb Addons: Blocks, Patterns, Pre-built Pages, Sliders, Popups, Free Forms, Animations & More v4.0.7
4.0.7 4.0.6 4.0.5 4.0.4 4.0.3 4.0.2 4.0.1 4.0.0 trunk 1.0.0 2.0.0 2.0.1 2.0.2 2.0.3 3.0 3.0.1 3.0.2 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.2 3.1.3 3.2.0 3.2.1 3.2.2 3.2.4 3.2.5 3.2.7 3.2.8 3.2.9 3.3.0 3.3.1 3.3.2 3.4.0 3.4.1 3.4.2 3.4.5 3.4.6 3.5.0 3.5.1 3.5.2 3.5.3 3.5.4 3.5.6 3.5.7 3.5.8 3.5.9 3.6.0 3.6.1 3.6.2 3.7.0 3.7.1
superb-blocks / src / admin / controllers / wizard / class-wizard-template-preview-controller.php
superb-blocks / src / admin / controllers / wizard Last commit date
class-wizard-controller.php 6 days ago class-wizard-restorationpoint-controller.php 6 days ago class-wizard-template-preview-controller.php 6 days ago
class-wizard-template-preview-controller.php
411 lines
1 <?php
2
3 namespace SuperbAddons\Admin\Controllers\Wizard;
4
5 use SuperbAddons\Config\Capabilities;
6 use SuperbAddons\Data\Controllers\CacheController;
7 use SuperbAddons\Data\Utils\AllowedTemplateHTMLUtil;
8 use SuperbAddons\Data\Utils\CacheTypes;
9 use SuperbAddons\Data\Utils\GutenbergCache;
10 use SuperbAddons\Data\Utils\ThirdPartyCompatibilityUtil;
11 use SuperbAddons\Data\Utils\Wizard\AddonsPageTemplateUtil;
12 use SuperbAddons\Data\Utils\Wizard\WizardItemIdAffix;
13 use SuperbAddons\Data\Utils\Wizard\WizardItemTypes;
14 use WP_Query;
15
16 defined('ABSPATH') || exit();
17
18 class WizardTemplatePreviewController
19 {
20 const TEMPLATE_PREVIEW_KEY = 'superbaddons-template-preview';
21 const TEMPLATE_TYPE_KEY = 'superbaddons-template-type';
22 const USE_PAGE_TEMPLATE_KEY = 'superbaddons-template-custom';
23 const TEMPLATE_PART_PREVIEW_TRANSIENT = 'superbaddons_template_part_preview';
24
25 const TEMPLATE_PREVIEW_NONCE = 'superbaddons-template-preview-nonce';
26 const TEMPLATE_PREVIEW_NONCE_ACTION = 'superbaddons-template-preview-action';
27
28 public static function InitializeTemplatePreview()
29 {
30 if (is_admin()) {
31 return;
32 }
33
34 add_action('wp_loaded', function () {
35 if (!isset($_GET[self::TEMPLATE_PREVIEW_KEY]) || !isset($_GET[self::TEMPLATE_TYPE_KEY]) || !isset($_GET[self::USE_PAGE_TEMPLATE_KEY]) || !isset($_GET[self::TEMPLATE_PREVIEW_NONCE])) {
36 return;
37 }
38 if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_GET[self::TEMPLATE_PREVIEW_NONCE])), self::TEMPLATE_PREVIEW_NONCE_ACTION)) {
39 return;
40 }
41 if (!current_user_can(Capabilities::ADMIN)) {
42 return;
43 }
44 $template_id = sanitize_text_field(wp_unslash($_GET[self::TEMPLATE_PREVIEW_KEY]));
45 $template_type = sanitize_text_field(wp_unslash($_GET[self::TEMPLATE_TYPE_KEY]));
46 $template_custom = $_GET[self::USE_PAGE_TEMPLATE_KEY] == 1;
47 show_admin_bar(false);
48 switch ($template_type) {
49 case WizardItemTypes::WP_TEMPLATE:
50 self::SetPlaceholderFilters($template_id, $template_type);
51 self::PreviewBlockTemplate($template_id, $template_type, $template_custom);
52 exit();
53 case WizardItemTypes::PATTERN:
54 case WizardItemTypes::PAGE:
55 self::SetPlaceholderFilters($template_id, $template_type);
56 self::PreviewSuperbTemplate($template_id, $template_type, $template_custom);
57 exit();
58 case WizardItemTypes::WP_TEMPLATE_PART:
59 self::PreviewBlockTemplate($template_id, $template_type, $template_custom);
60 exit();
61 default:
62 // Static preview
63 break;
64 }
65 });
66 }
67
68 private static function SetPlaceholderFilters($template_id, $template_type)
69 {
70 if ($template_type == WizardItemTypes::WP_TEMPLATE_PART || $template_type == WizardItemTypes::PATTERN) {
71 // Enqueue part preview specific styles
72 wp_enqueue_style('superbaddons-wizard-part-preview-styles', SUPERBADDONS_ASSETS_PATH . '/css/page-wizard-preview.min.css', array(), SUPERBADDONS_VERSION);
73 }
74 // Filter the header and footer parts to show a placeholder image instead of the actual content if a preview is requested instead of default theme header/footer.
75 self::FilterHeaderFooterPreviewParts();
76
77 // Set the post ID to 0 to prevent the preview from showing the actual content of the page and to prevent Undefined index: postId in /wp-includes/blocks/comments.php on line 31
78 add_filter("render_block_context", function ($context) {
79 if (isset($context['postId'])) {
80 return $context;
81 }
82 $context['postId'] = 0;
83 return $context;
84 });
85
86 add_filter('the_title', function ($title) use ($template_type, $template_id) {
87 if (!empty($title)) {
88 return $title;
89 }
90 if ($template_type === WizardItemTypes::WP_TEMPLATE) {
91 $template = get_block_template($template_id, WizardItemTypes::WP_TEMPLATE);
92 if ($template) {
93 return $template->title;
94 }
95 }
96
97 return esc_html__("Placeholder Title", "superb-blocks");
98 });
99
100 // Replace the post content with a placeholder content for preview
101 add_filter("the_content", function ($content) {
102 return sprintf('<!-- wp:heading {"level":1,"style":{"spacing":{"padding":{"top":"50px","bottom":"50px","left":"50px","right":"50px"}}}} --> <h1 class="wp-block-heading" style="padding-top:50px;padding-right:50px;padding-bottom:50px;padding-left:50px">%s</h1> <!-- /wp:heading -->', esc_html__("This is a preview of the template. Any page content that you create will appear here.", "superb-blocks"));
103 }, 10);
104
105 // Replace the posts with a placeholder title and placeholder content for preview
106 add_filter('the_posts', function ($posts, $query) {
107 if (!isset($query->query)) {
108 return $posts;
109 }
110
111 if (isset($query->query['post_type']) && $query->query['post_type'] !== 'post') {
112 return $posts;
113 }
114
115 $amount = isset($query->query['posts_per_page']) ? $query->query['posts_per_page'] : 3;
116 $amount = ($amount === -1) ? 3 : min($amount, 10);
117
118 $placeholder_posts = array();
119 for ($i = 0; $i < $amount; $i++) {
120 // Create a placeholder post
121 $placeholder_posts[] = (object)array(
122 'post_title' => esc_html__("Placeholder Post", "superb-blocks") . " " . ($i + 1),
123 'post_content' => '',
124 'post_status' => 'publish',
125 'post_type' => 'post',
126 'post_author' => 1,
127 'post_date' => gmdate('Y-m-d H:i:s'),
128 );
129 }
130 return $placeholder_posts;
131 }, 10, 2);
132
133 // Replace the post thumbnail with a placeholder for preview
134 add_filter("post_thumbnail_html", function () {
135 return self::GetPlaceholderPreviewImage();
136 }, 0, 0);
137
138 // Replace featured image in "Featured Image" blocks with a placeholder for preview
139 add_filter("render_block", function ($block_content, $block) {
140 if (isset($block['blockName']) && $block['blockName'] === 'core/post-featured-image' && empty($block_content)) {
141 return '<figure class="alignwide wp-block-post-featured-image">' . self::GetPlaceholderPreviewImage() . '</figure>';
142 }
143 return $block_content;
144 }, 10, 2);
145
146 // Start global wp_query query to make sure placeholder posts are shown.
147 global $wp_query;
148 if (!$wp_query || !$wp_query instanceof WP_Query) {
149 return;
150 }
151 $wp_query = new WP_Query([
152 'post_type' => 'post',
153 ]);
154 }
155
156 private static function GetPlaceholderPreviewImage()
157 {
158 return '<img width="1900" height="1267" src="' . esc_url(SUPERBADDONS_ASSETS_PATH . "/img/placeholder-image.svg") . '" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" style="height:100%;object-fit:cover;">';
159 }
160
161 private static function PreviewBlockTemplate($template_id, $template_type = WizardItemTypes::WP_TEMPLATE)
162 {
163 if (strpos($template_id, WizardItemIdAffix::FILE_TEMPLATE) !== false) {
164 // If the template is a file template, get the file template preview.
165 $template_id = str_replace(WizardItemIdAffix::FILE_TEMPLATE, '', $template_id);
166 self::PreviewBlockFileTemplate($template_id, $template_type);
167 return;
168 }
169
170 if (strpos($template_id, WizardItemIdAffix::RESTORATION_POINT) !== false) {
171 // If the template is a restoration point, get the restoration point preview.
172 $template_id = str_replace(WizardItemIdAffix::RESTORATION_POINT, '', $template_id);
173 self::PreviewRestorationPoint($template_id);
174 return;
175 }
176
177 $template = get_block_template($template_id, $template_type);
178 if (!$template || $template->type !== $template_type || $template->status !== 'publish' || $template->theme !== get_stylesheet() || empty($template->content)) {
179 return;
180 }
181
182 $template_html = self::GetTheTemplateHTML($template->content);
183
184 self::RenderTemplateCanvas($template_html);
185 }
186
187 private static function PreviewBlockFileTemplate($template_id, $template_type)
188 {
189 $template = get_block_file_template($template_id, $template_type);
190 if ($template->theme !== get_stylesheet() || empty($template->content)) {
191 return;
192 }
193
194 $template_html = self::GetTheTemplateHTML($template->content);
195
196 self::RenderTemplateCanvas($template_html);
197 }
198
199 private static function PreviewRestorationPoint($template_id)
200 {
201 $restoration_point = WizardRestorationPointController::GetTemplateRestorationPoint($template_id);
202 if (!$restoration_point) return;
203
204 $template_html = self::GetTheTemplateHTML($restoration_point['content']);
205
206 self::RenderTemplateCanvas($template_html);
207 }
208
209 private static function PreviewSuperbTemplate($template_id, $template_type = WizardItemTypes::PAGE, $template_custom = false)
210 {
211 // The cache has already been set, otherwise the preview will not work and we shouldn't be here.
212 $cache = self::GetLibraryCache();
213 if (!$cache) return;
214
215 switch ($template_type) {
216 case WizardItemTypes::PAGE:
217 $items = isset($cache->pages->items) ? $cache->pages->items : null;
218 break;
219 case WizardItemTypes::PATTERN:
220 $items = isset($cache->patterns->items) ? $cache->patterns->items : null;
221 break;
222 default:
223 return;
224 }
225 if (!$items) return;
226
227 $preview_url = false;
228 foreach ($items as $template) {
229 if ($template->id === $template_id) {
230 $preview_url = $template->preview;
231 break;
232 }
233 }
234
235 if (!$preview_url) return;
236
237 $preview_content = '<img src="' . esc_url($preview_url) . '" class="superbaddons-preview-image" width="100%" height="auto" style="user-select:none;">';
238
239 if ($template_type === WizardItemTypes::PAGE) {
240 // Override the content with a placeholder content for preview
241 if ($template_custom) {
242 add_filter("the_content", function () use ($preview_content) {
243 return $preview_content;
244 }, PHP_INT_MAX);
245 $template = get_block_template(get_stylesheet() . '//' . AddonsPageTemplateUtil::TEMPLATE_ID, WizardItemTypes::WP_TEMPLATE);
246 $content = $template->content;
247 } else {
248 $content = AddonsPageTemplateUtil::GetAddonsPageTemplateContent($preview_content);
249 }
250 } else {
251 // Preview pattern only
252 $content = $preview_content;
253 }
254
255 $template_html = self::GetTheTemplateHTML($content);
256
257 self::RenderTemplateCanvas($template_html);
258 }
259
260 private static function FilterHeaderFooterPreviewParts()
261 {
262 // track which areas are currently being processed to prevent infinite loops when a template part contains nested template parts
263 $processing_areas = [];
264
265 add_filter('render_block', function ($block_content, $block) use (&$processing_areas) {
266 if ($block['blockName'] !== 'core/template-part') {
267 return $block_content;
268 }
269
270 $attrs = $block['attrs'];
271 $template_id = (isset($attrs['theme']) ? $attrs['theme'] : get_stylesheet()) . '//' . $attrs['slug'];
272 $template = get_block_template($template_id, 'wp_template_part');
273
274 if (!$template || ($template->area !== 'header' && $template->area !== 'footer')) {
275 return $block_content;
276 }
277
278 // Prevent infinite loop for nested template parts: if we're already processing this area, return original content
279 if (isset($processing_areas[$template->area])) {
280 return $block_content;
281 }
282
283 $template_part_preview = WizardController::GetPartPreviewTransient();
284 if (!$template_part_preview || !isset($template_part_preview[$template->area])) {
285 return $block_content;
286 }
287
288 // Mark this area as being processed
289 $processing_areas[$template->area] = true;
290
291 // Get the preview content
292 $preview_content = WizardTemplatePreviewController::GetTemplatePartPreview($template->area);
293
294 // Unmark this area as being processed
295 unset($processing_areas[$template->area]);
296
297 if (!$preview_content) {
298 return $block_content;
299 }
300
301 return $preview_content;
302 }, 10, 2);
303 }
304
305 private static function GetTemplatePartPreview($slug)
306 {
307 $template_part_preview = WizardController::GetPartPreviewTransient();
308 if (!$template_part_preview || !isset($template_part_preview[$slug])) {
309 return false;
310 }
311
312 // Check if it's a file template preview
313 if (strpos($template_part_preview[$slug], WizardItemIdAffix::FILE_TEMPLATE) !== false) {
314 $template_id = get_stylesheet() . '//' . str_replace(WizardItemIdAffix::FILE_TEMPLATE, '', $template_part_preview[$slug]);
315 $template = get_block_file_template($template_id, WizardItemTypes::WP_TEMPLATE_PART);
316 return self::GetTheTemplateHTML($template->content);
317 }
318
319 // Check if it's a restoration point preview
320 if (strpos($template_part_preview[$slug], WizardItemIdAffix::RESTORATION_POINT) !== false) {
321 $restoration_id = str_replace(WizardItemIdAffix::RESTORATION_POINT, '', $template_part_preview[$slug]);
322 $restoration_point = WizardRestorationPointController::GetTemplateRestorationPoint($restoration_id);
323 if (!$restoration_point) return false;
324
325 return self::GetTheTemplateHTML($restoration_point['content']);
326 }
327
328 // Avoid further handling of theme slug templates
329 if (strpos($slug, get_stylesheet()) !== false) {
330 return false;
331 }
332
333 // Try to get as file template first
334 $template_id = get_stylesheet() . '//' . $template_part_preview[$slug];
335 $template = get_block_file_template($template_id, WizardItemTypes::WP_TEMPLATE_PART);
336 if ($template && !empty($template->content)) {
337 return self::GetTheTemplateHTML($template->content);
338 }
339
340 // Try regular template next
341 $template = get_block_template($template_id, WizardItemTypes::WP_TEMPLATE_PART);
342 if ($template && !empty($template->content)) {
343 return self::GetTheTemplateHTML($template->content);
344 }
345
346 // If not a file template, check patterns cache
347 $cache = self::GetLibraryCache();
348 if (!$cache || !isset($cache->patterns->items)) return false;
349
350 $preview_url = false;
351 foreach ($cache->patterns->items as $template) {
352 if ($template->id === $template_part_preview[$slug]) {
353 $preview_url = $template->preview;
354 break;
355 }
356 }
357
358 if (!$preview_url) return false;
359
360 return '<img src="' . esc_url($preview_url) . '" class="superbaddons-preview-image" width="100%" height="auto" style="user-select:none;">';
361 }
362
363 private static function GetLibraryCache()
364 {
365 $cache = CacheController::GetCache(GutenbergCache::LIBRARY, CacheTypes::GUTENBERG);
366 if ($cache) return $cache;
367 // Fall back to partial cache while chunks are still loading
368 return CacheController::GetDataCacheDirect(GutenbergCache::LIBRARY_PARTIAL);
369 }
370
371 private static function GetTheTemplateHTML($content)
372 {
373 global $_wp_current_template_content;
374 // Variable is not defined by plugin, but is a global WP variable.
375 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
376 $_wp_current_template_content = $content;
377 return get_the_block_template_html();
378 }
379
380 private static function RenderTemplateCanvas($content)
381 {
382 // Register shims for known third-party conflicts (e.g. Gutenverse) so the preview renders fully.
383 ThirdPartyCompatibilityUtil::guard_preview_render();
384
385 ?>
386 <!DOCTYPE html>
387 <html <?php language_attributes(); ?>>
388
389 <head>
390 <meta charset="<?php bloginfo('charset'); ?>" />
391 <meta name="viewport" content="width=device-width, initial-scale=1" />
392 <?php
393 // Run hook-firing calls through a safety net so an uncaught error from any third-party
394 // hook degrades the preview gracefully instead of white-screening it.
395 ThirdPartyCompatibilityUtil::run_guarded('wp_head');
396 ?>
397 </head>
398
399 <body <?php body_class(); ?>>
400 <?php ThirdPartyCompatibilityUtil::run_guarded('wp_body_open'); ?>
401 <?php AllowedTemplateHTMLUtil::enable_safe_styles(); ?>
402 <?php echo wp_kses($content, "post"); ?>
403 <?php AllowedTemplateHTMLUtil::disable_safe_styles(); ?>
404 <?php ThirdPartyCompatibilityUtil::run_guarded('wp_footer'); ?>
405 </body>
406
407 </html>
408 <?php
409 }
410 }
411