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