Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.22% covered (warning)
77.22%
61 / 79
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Wpcom_Block_Patterns_From_Api
78.21% covered (warning)
78.21%
61 / 78
60.00% covered (warning)
60.00%
3 / 5
42.60
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 register_patterns
100.00% covered (success)
100.00%
41 / 41
100.00% covered (success)
100.00%
1 / 1
11
 get_patterns
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 can_register_pattern
28.57% covered (danger)
28.57%
2 / 7
0.00% covered (danger)
0.00%
0 / 1
14.11
 update_pattern_post_types
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2/**
3 * Class Wpcom Block Patterns From Api
4 *
5 * @package automattic/jetpack-mu-wpcom
6 */
7
8/**
9 * Require the utils class.
10 */
11require_once __DIR__ . '/class-wpcom-block-patterns-utils.php';
12
13/**
14 * Class Wpcom_Block_Patterns_From_Api
15 */
16class Wpcom_Block_Patterns_From_Api {
17    const PATTERN_NAMESPACE = 'a8c/';
18
19    /**
20     * A collection of utility methods.
21     *
22     * @var Wpcom_Block_Patterns_Utils
23     */
24    private $utils;
25
26    /**
27     * Block_Patterns constructor.
28     *
29     * @param Wpcom_Block_Patterns_Utils|null $utils       A class dependency containing utils methods.
30     */
31    public function __construct( ?Wpcom_Block_Patterns_Utils $utils = null ) {
32        $this->utils = empty( $utils ) ? new Wpcom_Block_Patterns_Utils() : $utils;
33    }
34
35    /**
36     * Register FSE block patterns and categories.
37     *
38     * @return array Results of pattern registration.
39     */
40    public function register_patterns() {
41        // Used to track which patterns we successfully register.
42        $results = array();
43
44        $patterns_cache_key = $this->utils->get_patterns_cache_key();
45
46        $pattern_categories = array();
47        $block_patterns     = $this->get_patterns( $patterns_cache_key );
48
49        // Register categories from first pattern in each category.
50        foreach ( (array) $block_patterns as $pattern ) {
51            foreach ( (array) $pattern['categories'] as $slug => $category ) {
52                // Skip categories that start with an underscore
53                $is_hidden_category = substr( $slug, 0, 1 ) === '_';
54
55                if ( ! isset( $pattern_categories[ $slug ] ) && ! $is_hidden_category ) {
56                    $pattern_categories[ $slug ] = array(
57                        'label'       => $category['title'],
58                        'description' => $category['description'],
59                    );
60
61                    // Unregister first to overwrite any existent categories
62                    unregister_block_pattern_category( $slug );
63                    register_block_pattern_category(
64                        $slug,
65                        $pattern_categories[ $slug ]
66                    );
67                }
68            }
69        }
70
71        foreach ( (array) $block_patterns as &$pattern ) {
72            if ( $this->can_register_pattern( $pattern ) ) {
73                $is_premium = isset( $pattern['pattern_meta']['is_premium'] ) ? boolval( $pattern['pattern_meta']['is_premium'] ) : false;
74
75                // Set custom viewport width for the pattern preview with a
76                // default width of 1280 and ensure a safe minimum width of 320.
77                $viewport_width = isset( $pattern['pattern_meta']['viewport_width'] ) ? intval( $pattern['pattern_meta']['viewport_width'] ) : 1280;
78                $viewport_width = $viewport_width < 320 ? 320 : $viewport_width;
79                $pattern_name   = self::PATTERN_NAMESPACE . $pattern['name'];
80                $block_types    = $this->utils->maybe_get_pattern_block_types_from_pattern_meta( $pattern );
81                if ( empty( $block_types ) ) {
82                    // For wp_block patterns because don't use pattern meta for block types.
83                    $block_types = $this->utils->get_block_types_from_categories( $pattern );
84                }
85
86                $results[ $pattern_name ] = register_block_pattern(
87                    $pattern_name,
88                    array(
89                        'title'         => $pattern['title'],
90                        'description'   => $pattern['description'],
91                        'content'       => $pattern['html'],
92                        'viewportWidth' => $viewport_width,
93                        'categories'    => array_keys(
94                            $pattern['categories']
95                        ),
96                        'isPremium'     => $is_premium,
97                        'blockTypes'    => $block_types,
98                    )
99                );
100            }
101        }
102
103        // Temporarily removing the call to `update_pattern_post_types` while we investigate
104        // https://github.com/Automattic/wp-calypso/issues/79145.
105
106        return $results;
107    }
108
109    /**
110     * Returns a list of patterns.
111     *
112     * @param string $patterns_cache_key Key to store responses to and fetch responses from cache.
113     * @return array The list of patterns.
114     */
115    private function get_patterns( $patterns_cache_key ) {
116        $override_source_site = apply_filters( 'a8c_override_patterns_source_site', false );
117
118        $block_patterns = $this->utils->cache_get( $patterns_cache_key, 'ptk_patterns' );
119        $disable_cache  = ( function_exists( 'is_automattician' ) && is_automattician() ) || $override_source_site || ( defined( 'WP_DISABLE_PATTERN_CACHE' ) && WP_DISABLE_PATTERN_CACHE );
120
121        // Load fresh data if is automattician or we don't have any data.
122        if ( $disable_cache || false === $block_patterns ) {
123            $request_url = esc_url_raw(
124                add_query_arg(
125                    array(
126                        'site'      => $override_source_site ?? 'dotcompatterns.wordpress.com',
127                        'post_type' => 'wp_block',
128                    ),
129                    'https://public-api.wordpress.com/rest/v1/ptk/patterns/' . $this->utils->get_block_patterns_locale()
130                )
131            );
132
133            $block_patterns = $this->utils->remote_get( $request_url );
134
135            // Only save to cache when is not disabled.
136            if ( ! $disable_cache ) {
137                $this->utils->cache_add( $patterns_cache_key, $block_patterns, 'ptk_patterns', 5 * MINUTE_IN_SECONDS );
138            }
139        }
140
141        return $block_patterns;
142    }
143
144    /**
145     * Check that the pattern is allowed to be registered.
146     *
147     * Checks for pattern_meta tags with a prefix of `requires-` in the name, and then attempts to match
148     * the remainder of the name to a theme feature.
149     *
150     * For example, to prevent patterns that depend on wide or full-width block alignment support
151     * from being registered in sites where the active theme does not have `align-wide` support,
152     * we can add the `requires-align-wide` pattern_meta tag to the pattern. This function will
153     * then match against that pattern_meta tag, and then return `false`.
154     *
155     * @param array $pattern    A pattern with a 'pattern_meta' array where the key is the tag slug in English.
156     *
157     * @return bool
158     */
159    private function can_register_pattern( $pattern ) {
160        if ( empty( $pattern['pattern_meta'] ) ) {
161            // Default to allowing patterns without metadata to be registered.
162            return true;
163        }
164
165        foreach ( $pattern['pattern_meta'] as $pattern_meta => $value ) {
166            // Match against tags with a non-translated slug beginning with `requires-`.
167            $split_slug = preg_split( '/^requires-/', $pattern_meta );
168
169            // If the theme does not support the matched feature, then skip registering the pattern.
170            if ( isset( $split_slug[1] ) && false === get_theme_support( $split_slug[1] ) ) {
171                return false;
172            }
173        }
174
175        return true;
176    }
177
178    /**
179     * Ensure that all patterns with a blockType property are registered with appropriate postTypes.
180     */
181    private function update_pattern_post_types() {
182        if ( ! class_exists( 'WP_Block_Patterns_Registry' ) ) {
183            return;
184        }
185        foreach ( \WP_Block_Patterns_Registry::get_instance()->get_all_registered() as $pattern ) {
186            if ( array_key_exists( 'postTypes', $pattern ) && $pattern['postTypes'] ) {
187                continue;
188            }
189
190            $post_types = $this->utils->get_pattern_post_types_from_pattern( $pattern );
191            if ( $post_types ) {
192                unregister_block_pattern( $pattern['name'] );
193
194                $pattern['postTypes'] = $post_types;
195                $pattern_name         = $pattern['name'];
196                unset( $pattern['name'] );
197                register_block_pattern( $pattern_name, $pattern );
198            }
199        }
200    }
201}