PluginProbe
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 3.4.1
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v3.4.1
4.7.2 4.7.1 4.7.0 4.6.6 4.6.5 4.6.4 4.6.3 4.6.2 4.6.1 4.6.0 4.5.1 4.5.0 4.4.2 4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.3 4.2.2 4.2.1 1.0.3 1.0.4 1.0.5 All 281 releases
surecart / app / src / Support / Blocks / TemplateUtilityService.php
TemplateUtilityService.php
499 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SureCart\Support\Blocks;
4
5 /**
6 * The block templates service.
7 */
8 class TemplateUtilityService {
9 /**
10 * Holds the path for the directory where the block templates will be kept.
11 *
12 * @var string
13 */
14 private $templates_directory;
15
16 /**
17 * Holds the path for the directory where the block template parts will be kept.
18 *
19 * @var string
20 */
21 private $template_parts_directory;
22
23 /**
24 * The product template types.
25 *
26 * @var array
27 */
28 private $plugin_template_types;
29
30 /**
31 * SureCart plugin slug
32 *
33 * This is used to save templates to the DB which are stored against this value in the wp_terms table.
34 *
35 * @var string
36 */
37 const PLUGIN_SLUG = 'surecart/surecart';
38
39 /**
40 * Set the directories where the block templates will be kept.
41 *
42 * @param string $templates_directory The path for the directory where the block templates will be kept.
43 * @param string $template_parts_directory The path for the directory where the block template parts will be kept.
44 */
45 public function __construct( $templates_directory, $template_parts_directory ) {
46 $this->templates_directory = $templates_directory;
47 $this->template_parts_directory = $template_parts_directory;
48 $this->plugin_template_types = array(
49 'single-sc_product' => array(
50 'title' => class_exists( 'WooCommerce' ) ? _x( 'SureCart Products', 'Template name', 'surecart' ) : _x( 'Products', 'Template name', 'surecart' ),
51 'description' => __( 'Display all individual products unless a custom template has been applied.', 'surecart' ),
52 'post_types' => array( 'sc_product' ),
53 ),
54 'product-info' => array(
55 'title' => class_exists( 'WooCommerce' ) ? _x( 'SureCart Products', 'Template name', 'surecart' ) : _x( 'Products', 'Template name', 'surecart' ),
56 'description' => __( 'Display all individual products content unless a custom template has been applied.', 'surecart' ),
57 'site-editor' => false,
58 'post_types' => array( 'sc_product' ),
59 ),
60 'single-upsell' => array(
61 'title' => _x( 'Upsells', 'Template name', 'surecart' ),
62 'description' => __( 'Display all individual upsells unless a custom template has been applied.', 'surecart' ),
63 'post_types' => array( 'sc_upsell' ),
64 ),
65 'upsell-info' => array(
66 'title' => _x( 'Upsells', 'Template name', 'surecart' ),
67 'description' => __( 'Display all individual upsells content unless a custom template has been applied.', 'surecart' ),
68 'site-editor' => false,
69 'post_types' => array( 'sc_upsell' ),
70 ),
71 'taxonomy-sc_collection' => array(
72 'title' => class_exists( 'WooCommerce' ) ? _x( 'SureCart Product Collections', 'Template name', 'surecart' ) : _x( 'Product Collections', 'Template name', 'surecart' ),
73 'description' => __( 'Display all individual product collections unless a custom template has been applied.', 'surecart' ),
74 ),
75 'product-collection-part' => array(
76 'title' => class_exists( 'WooCommerce' ) ? _x( 'SureCart Product Collections', 'Template name', 'surecart' ) : _x( 'Product Collections', 'Template name', 'surecart' ),
77 'description' => __( 'Display all individual product collections content unless a custom template has been applied.', 'surecart' ),
78 'site-editor' => false,
79 ),
80 'cart' => array(
81 'title' => class_exists( 'WooCommerce' ) ? _x( 'SureCart Cart', 'Template name', 'surecart' ) : _x( 'Cart', 'Template name', 'surecart' ),
82 'description' => __( 'The slide-out cart template.', 'surecart' ),
83 ),
84 'checkout' => array(
85 'title' => class_exists( 'WooCommerce' ) ? _x( 'SureCart Checkout', 'Template name', 'surecart' ) : _x( 'Checkout', 'Template name', 'surecart' ),
86 'description' => __( 'Display the checkout content unless a custom template has been applied.', 'surecart' ),
87 ),
88 );
89 }
90
91 /**
92 * Gets the first matching template part within themes directories
93 *
94 * Since [Gutenberg 12.1.0](https://github.com/WordPress/gutenberg/releases/tag/v12.1.0), the conventions for
95 * block templates and parts directory has changed from `block-templates` and `block-templates-parts`
96 * to `templates` and `parts` respectively.
97 *
98 * This function traverses all possible combinations of directory paths where a template or part
99 * could be located and returns the first one which is readable, prioritizing the new convention
100 * over the deprecated one, but maintaining that one for backwards compatibility.
101 *
102 * @param string $template_slug The slug of the template (i.e. without the file extension).
103 * @param string $template_type Either `wp_template` or `wp_template_part`.
104 *
105 * @return string|null The matched path or `null` if no match was found.
106 */
107 public function getThemeTemplatePath( $template_slug, $template_type = 'wp_template' ) {
108 $template_filename = $template_slug . '.html';
109 $templates_dir = 'wp_template' === $template_type ? 'templates' : 'parts';
110
111 $filepath = DIRECTORY_SEPARATOR . $templates_dir . DIRECTORY_SEPARATOR . $template_filename;
112
113 $possible_paths = array(
114 get_stylesheet_directory() . $filepath,
115 get_template_directory() . $filepath,
116 );
117
118 // Return the first matching.
119 foreach ( $possible_paths as $path ) {
120 if ( is_readable( $path ) ) {
121 return $path;
122 }
123 }
124
125 return null;
126 }
127
128 /**
129 * Gets the directory where templates of a specific template type can be found.
130 *
131 * @param string $template_type wp_template or wp_template_part.
132 *
133 * @return string
134 */
135 public function getTemplatesDirectory( $template_type = 'wp_template' ) {
136 return 'wp_template_part' === $template_type ? $this->template_parts_directory : $this->templates_directory;
137 }
138
139 /**
140 * Finds all nested template part file paths in a theme's directory.
141 *
142 * @param string $base_directory The theme's file path.
143 * @return array $path_list A list of paths to all template part files.
144 */
145 public function getTemplatePaths( $base_directory ) {
146 $path_list = array();
147 if ( file_exists( $base_directory ) ) {
148 $nested_files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $base_directory ) );
149 $nested_html_files = new \RegexIterator( $nested_files, '/^.+\.html$/i', \RecursiveRegexIterator::GET_MATCH );
150 foreach ( $nested_html_files as $path => $file ) {
151 $path_list[] = $path;
152 }
153 }
154 return $path_list;
155 }
156
157 /**
158 * Converts template paths into a slug
159 *
160 * @param string $path The template's path.
161 * @return string slug
162 */
163 public function generateTemplateSlugFromPath( $path ) {
164 return basename( $path, '.html' );
165 }
166
167 /**
168 * Check if the theme has a template. So we know if to load our own in or not.
169 *
170 * @param string $template_name name of the template file without .html extension e.g. 'single-product'.
171 * @return boolean
172 */
173 public function themeHasTemplate( $template_name ) {
174 return (bool) $this->getThemeTemplatePath( $template_name, 'wp_template' );
175 }
176
177 /**
178 * Check if the theme has a template. So we know if to load our own in or not.
179 *
180 * @param string $template_name name of the template file without .html extension e.g. 'single-product'.
181 * @return boolean
182 */
183 public function themeHasTemplatePart( $template_name ) {
184 return (bool) $this->getThemeTemplatePath( $template_name, 'wp_template_part' );
185 }
186
187 /**
188 * Is this a FSE theme?
189 *
190 * @return boolean
191 */
192 public function isFSETheme() {
193 if ( function_exists( 'wp_is_block_theme' ) ) {
194 return (bool) \wp_is_block_theme();
195 }
196 if ( function_exists( 'gutenberg_is_fse_theme' ) ) {
197 return (bool) \gutenberg_is_fse_theme();
198 }
199 return false;
200 }
201
202 /**
203 * Does this theme support block templates?
204 *
205 * @return boolean
206 */
207 public function supportsBlockTemplates() {
208 return $this->isFSETheme() || ( function_exists( 'gutenberg_supports_block_templates' ) && \gutenberg_supports_block_templates() );
209 }
210
211
212 /**
213 * Build a unified template object based a post Object.
214 * Important: This method is an almost identical duplicate from wp-includes/block-template-utils.php as it was not intended for public use. It has been modified to build templates from plugins rather than themes.
215 *
216 * @param \WP_Post $post Template post.
217 *
218 * @return \WP_Block_Template|\WP_Error Template.
219 */
220 public function buildTemplateResultsFromPost( $post ) {
221 $terms = get_the_terms( $post, 'wp_theme' );
222
223 if ( is_wp_error( $terms ) ) {
224 return $terms;
225 }
226
227 if ( ! $terms ) {
228 return new \WP_Error( 'template_missing_theme', __( 'No theme is defined for this template.', 'surecart' ) );
229 }
230
231 $theme = $terms[0]->name;
232 $has_theme_file = true;
233
234 $template = new \WP_Block_Template();
235 $template->wp_id = $post->ID;
236 $template->id = $theme . '//' . $post->post_name;
237 $template->theme = $theme;
238 $template->content = $post->post_content;
239 $template->slug = $post->post_name;
240 $template->source = 'custom';
241 $template->type = $post->post_type;
242 $template->description = $post->post_excerpt;
243 $template->title = $post->post_title;
244 $template->status = $post->post_status;
245 $template->has_theme_file = $has_theme_file;
246 $template->is_custom = true;
247 $template->post_types = array(); // don't appear in edit posts dropdown.
248
249 // this is a customized version of the template.
250 if ( ! empty( $this->plugin_template_types[ $post->post_name ]['post_types'] ) ) {
251 $template->post_types = $this->plugin_template_types[ $post->post_name ]['post_types'];
252 }
253
254 if ( 'wp_template_part' === $post->post_type ) {
255 $type_terms = get_the_terms( $post, 'wp_template_part_area' );
256 if ( ! is_wp_error( $type_terms ) && false !== $type_terms ) {
257 $template->area = $type_terms[0]->name;
258 }
259 }
260
261 if ( 'surecart/surecart' === $theme ) {
262 $template->origin = 'plugin';
263 }
264
265 return $template;
266 }
267
268 /**
269 * Returns an array containing the references of
270 * the passed blocks and their inner blocks.
271 *
272 * @param array $blocks array of blocks.
273 *
274 * @return array block references to the passed blocks and their inner blocks.
275 */
276 public function flattenBlocks( &$blocks ) {
277 $all_blocks = array();
278 $queue = array();
279 foreach ( $blocks as &$block ) {
280 $queue[] = &$block;
281 }
282 $queue_count = count( $queue );
283
284 while ( $queue_count > 0 ) {
285 $block = &$queue[0];
286 array_shift( $queue );
287 $all_blocks[] = &$block;
288
289 if ( ! empty( $block['innerBlocks'] ) ) {
290 foreach ( $block['innerBlocks'] as &$inner_block ) {
291 $queue[] = &$inner_block;
292 }
293 }
294
295 $queue_count = count( $queue );
296 }
297
298 return $all_blocks;
299 }
300
301 /**
302 * Parses wp_template content and injects the current theme's
303 * stylesheet as a theme attribute into each wp_template_part
304 *
305 * @param string $template_content serialized wp_template content.
306 *
307 * @return string Updated wp_template content.
308 */
309 public function injectThemeAttributeInContent( $template_content ) {
310 $has_updated_content = false;
311 $new_content = '';
312 $template_blocks = parse_blocks( $template_content );
313
314 $blocks = $this->flattenBlocks( $template_blocks );
315 foreach ( $blocks as &$block ) {
316 if (
317 'core/template-part' === $block['blockName'] &&
318 ! isset( $block['attrs']['theme'] )
319 ) {
320 $block['attrs']['theme'] = wp_get_theme()->get_stylesheet();
321 $has_updated_content = true;
322 }
323 }
324
325 if ( $has_updated_content ) {
326 foreach ( $template_blocks as &$block ) {
327 $new_content .= serialize_block( $block );
328 }
329
330 return $new_content;
331 }
332
333 return $template_content;
334 }
335
336 /**
337 * Build a unified template object based on a theme file.
338 * Important: This method is an almost identical duplicate from wp-includes/block-template-utils.php as it was not intended for public use. It has been modified to build templates from plugins rather than themes.
339 *
340 * @param array|object $template_file Theme file.
341 * @param string $template_type wp_template or wp_template_part.
342 *
343 * @return \WP_Block_Template Template.
344 */
345 public function buildTemplateResultFromFile( $template_file, $template_type ) {
346 $default_template_types = get_default_block_template_types();
347 $template_file = (object) $template_file;
348
349 // If the theme has an archive-products.html template but does not have product taxonomy templates
350 // then we will load in the archive-product.html template from the theme to use for product taxonomies on the frontend.
351 $template_is_from_theme = 'theme' === $template_file->source;
352 $theme_name = wp_get_theme()->get( 'TextDomain' );
353
354 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
355 $template_content = file_get_contents( $template_file->path );
356 $template = new \WP_Block_Template();
357 $template->id = $template_is_from_theme ? $theme_name . '//' . $template_file->slug : self::PLUGIN_SLUG . '//' . $template_file->slug;
358 $template->theme = $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG;
359 $template->content = $this->injectThemeAttributeInContent( $template_content );
360 // Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909.
361 $template->source = $template_file->source ? $template_file->source : 'plugin';
362 $template->slug = $template_file->slug;
363 $template->type = $template_type;
364 $template->title = ! empty( $template_file->title ) ? $template_file->title : $this->getBlockTemplateTitle( $template_file->slug );
365 $template->description = ! empty( $template_file->description ) ? $template_file->description : $this->getBlockTemplateDescription( $template_file->slug );
366 $template->status = 'publish';
367 $template->has_theme_file = true;
368 $template->origin = $template_file->source;
369 $template->is_custom = false; // Templates loaded from the filesystem aren't custom, ones that have been edited and loaded from the DB are.
370 $template->post_types = $template_file->post_types ?? array(); // Set the post type.
371 $template->area = 'uncategorized';
372
373 if ( 'wp_template' === $template_type && isset( $default_template_types[ $template_file->slug ] ) ) {
374 $template->is_custom = false;
375 }
376
377 return $template;
378 }
379
380 /**
381 * Returns template titles.
382 *
383 * @param string $template_slug The templates slug (e.g. single-product).
384 * @return string Human friendly title.
385 */
386 public function getBlockTemplateTitle( $template_slug ) {
387 if ( isset( $this->plugin_template_types[ $template_slug ] ) ) {
388 return $this->plugin_template_types[ $template_slug ]['title'];
389 }
390 // Human friendly title converted from the slug.
391 return ucwords( preg_replace( '/[\-_]/', ' ', $template_slug ) );
392 }
393
394 /**
395 * Returns template descriptions.
396 *
397 * @param string $template_slug The templates slug (e.g. single-product).
398 * @return string Template description.
399 */
400 public function getBlockTemplateDescription( $template_slug ) {
401 if ( isset( $this->plugin_template_types[ $template_slug ] ) ) {
402 return $this->plugin_template_types[ $template_slug ]['description'];
403 }
404 return '';
405 }
406
407 /**
408 * Returns whether a block template is available in the site editor.
409 *
410 * @param string $template_slug The templates slug (e.g. single-product).
411 *
412 * @return boolean
413 */
414 public function isBlockAvailableInSiteEditor( $template_slug ) {
415 if ( isset( $this->plugin_template_types[ $template_slug ] ) ) {
416 return ! isset( $this->plugin_template_types[ $template_slug ]['site-editor'] ) || $this->plugin_template_types[ $template_slug ]['site-editor'];
417 }
418 return true;
419 }
420
421 /**
422 * Build a new template object so that we can make SureCart Blocks default templates available in the current theme should they not have any.
423 *
424 * @param string $template_file Block template file path.
425 * @param string $template_type wp_template or wp_template_part.
426 * @param string $template_slug Block template slug e.g. single-product.
427 * @param bool $template_is_from_theme If the block template file is being loaded from the current theme instead of SureCart Blocks.
428 *
429 * @return object Block template object.
430 */
431 public function createNewBlockTemplateObject( $template_file, $template_type, $template_slug, $template_is_from_theme = false ) {
432 $theme_name = wp_get_theme()->get( 'TextDomain' );
433
434 $new_template_item = array(
435 'slug' => $template_slug,
436 'id' => $template_is_from_theme ? $theme_name . '//' . $template_slug : self::PLUGIN_SLUG . '//' . $template_slug,
437 'path' => $template_file,
438 'type' => $template_type,
439 'theme' => $template_is_from_theme ? $theme_name : self::PLUGIN_SLUG,
440 // Plugin was agreed as a valid source value despite existing inline docs at the time of creating: https://github.com/WordPress/gutenberg/issues/36597#issuecomment-976232909.
441 'source' => $template_is_from_theme ? 'theme' : 'plugin',
442 'title' => $this->getBlockTemplateTitle( $template_slug ),
443 'description' => $this->getBlockTemplateDescription( $template_slug ),
444 );
445
446 return (object) $new_template_item;
447 }
448
449 /**
450 * Removes templates that were added to a theme's block-templates directory, but already had a customised version saved in the database.
451 *
452 * @param \WP_Block_Template[]|\stdClass[] $templates List of templates to run the filter on.
453 *
454 * @return array List of templates with duplicates removed. The customised alternative is preferred over the theme default.
455 */
456 public static function removeThemeTemplatesWithCustomAlternative( $templates ) {
457
458 // Get the slugs of all templates that have been customised and saved in the database.
459 $customised_template_slugs = array_map(
460 function ( $template ) {
461 return $template->slug;
462 },
463 array_values(
464 array_filter(
465 $templates,
466 function ( $template ) {
467 // This template has been customised and saved as a post.
468 return 'custom' === $template->source;
469 }
470 )
471 )
472 );
473
474 // Remove theme (i.e. filesystem) templates that have the same slug as a customised one. We don't need to check
475 // for `woocommerce` in $template->source here because woocommerce templates won't have been added to $templates
476 // if a saved version was found in the db. This only affects saved templates that were saved BEFORE a theme
477 // template with the same slug was added.
478 return array_values(
479 array_filter(
480 $templates,
481 function ( $template ) use ( $customised_template_slugs ) {
482 // This template has been customised and saved as a post, so return it.
483 return ! ( 'theme' === $template->source && in_array( $template->slug, $customised_template_slugs, true ) );
484 }
485 )
486 );
487 }
488
489 /**
490 * Returns whether the passed `$template` has a title, and it's different from the slug.
491 *
492 * @param object $template The template object.
493 * @return boolean
494 */
495 public function templateHasTitle( $template ) {
496 return ! empty( $template->title ) && $template->title !== $template->slug;
497 }
498 }
499