Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.12% covered (warning)
79.12%
72 / 91
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Wpcom_Block_Patterns_From_Api
80.00% covered (warning)
80.00%
72 / 90
50.00% covered (danger)
50.00%
3 / 6
57.79
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%
42 / 42
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
 get_gutenpen_patterns
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
11.09
 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    const GUTENPEN_PATTERNS_SOURCE_SITE = 'gutenpenpatternsourcesite.wordpress.com';
19
20    /**
21     * A collection of utility methods.
22     *
23     * @var Wpcom_Block_Patterns_Utils
24     */
25    private $utils;
26
27    /**
28     * Block_Patterns constructor.
29     *
30     * @param Wpcom_Block_Patterns_Utils|null $utils       A class dependency containing utils methods.
31     */
32    public function __construct( ?Wpcom_Block_Patterns_Utils $utils = null ) {
33        $this->utils = empty( $utils ) ? new Wpcom_Block_Patterns_Utils() : $utils;
34    }
35
36    /**
37     * Register FSE block patterns and categories.
38     *
39     * @return array Results of pattern registration.
40     */
41    public function register_patterns() {
42        // Used to track which patterns we successfully register.
43        $results = array();
44
45        $patterns_cache_key = $this->utils->get_patterns_cache_key();
46
47        $pattern_categories = array();
48        $block_patterns     = $this->get_patterns( $patterns_cache_key );
49        $block_patterns     = array_merge( $block_patterns, $this->get_gutenpen_patterns() );
50
51        // Register categories from first pattern in each category.
52        foreach ( (array) $block_patterns as $pattern ) {
53            foreach ( (array) $pattern['categories'] as $slug => $category ) {
54                // Skip categories that start with an underscore
55                $is_hidden_category = substr( $slug, 0, 1 ) === '_';
56
57                if ( ! isset( $pattern_categories[ $slug ] ) && ! $is_hidden_category ) {
58                    $pattern_categories[ $slug ] = array(
59                        'label'       => $category['title'],
60                        'description' => $category['description'],
61                    );
62
63                    // Unregister first to overwrite any existent categories
64                    unregister_block_pattern_category( $slug );
65                    register_block_pattern_category(
66                        $slug,
67                        $pattern_categories[ $slug ]
68                    );
69                }
70            }
71        }
72
73        foreach ( (array) $block_patterns as &$pattern ) {
74            if ( $this->can_register_pattern( $pattern ) ) {
75                $is_premium = isset( $pattern['pattern_meta']['is_premium'] ) ? boolval( $pattern['pattern_meta']['is_premium'] ) : false;
76
77                // Set custom viewport width for the pattern preview with a
78                // default width of 1280 and ensure a safe minimum width of 320.
79                $viewport_width = isset( $pattern['pattern_meta']['viewport_width'] ) ? intval( $pattern['pattern_meta']['viewport_width'] ) : 1280;
80                $viewport_width = $viewport_width < 320 ? 320 : $viewport_width;
81                $pattern_name   = self::PATTERN_NAMESPACE . $pattern['name'];
82                $block_types    = $this->utils->maybe_get_pattern_block_types_from_pattern_meta( $pattern );
83                if ( empty( $block_types ) ) {
84                    // For wp_block patterns because don't use pattern meta for block types.
85                    $block_types = $this->utils->get_block_types_from_categories( $pattern );
86                }
87
88                $results[ $pattern_name ] = register_block_pattern(
89                    $pattern_name,
90                    array(
91                        'title'         => $pattern['title'],
92                        'description'   => $pattern['description'],
93                        'content'       => $pattern['html'],
94                        'viewportWidth' => $viewport_width,
95                        'categories'    => array_keys(
96                            $pattern['categories']
97                        ),
98                        'isPremium'     => $is_premium,
99                        'blockTypes'    => $block_types,
100                    )
101                );
102            }
103        }
104
105        // Temporarily removing the call to `update_pattern_post_types` while we investigate
106        // https://github.com/Automattic/wp-calypso/issues/79145.
107
108        return $results;
109    }
110
111    /**
112     * Returns a list of patterns.
113     *
114     * @param string $patterns_cache_key Key to store responses to and fetch responses from cache.
115     * @param string $source_site        Source site ID or domain.
116     * @return array The list of patterns.
117     */
118    private function get_patterns( $patterns_cache_key, $source_site = 'dotcompatterns.wordpress.com' ) {
119        $override_source_site = apply_filters( 'a8c_override_patterns_source_site', null );
120
121        $block_patterns = $this->utils->cache_get( $patterns_cache_key, 'ptk_patterns' );
122        $disable_cache  = ( function_exists( 'is_automattician' ) && is_automattician() ) || $override_source_site || ( defined( 'WP_DISABLE_PATTERN_CACHE' ) && WP_DISABLE_PATTERN_CACHE );
123
124        // Load fresh data if is automattician or we don't have any data.
125        if ( $disable_cache || false === $block_patterns ) {
126            $request_url = esc_url_raw(
127                add_query_arg(
128                    array(
129                        'site'      => $override_source_site ?? $source_site,
130                        'post_type' => 'wp_block',
131                    ),
132                    'https://public-api.wordpress.com/rest/v1/ptk/patterns/' . $this->utils->get_block_patterns_locale()
133                )
134            );
135
136            $block_patterns = $this->utils->remote_get( $request_url );
137
138            // Only save to cache when is not disabled.
139            if ( ! $disable_cache ) {
140                $this->utils->cache_add( $patterns_cache_key, $block_patterns, 'ptk_patterns', 5 * MINUTE_IN_SECONDS );
141            }
142        }
143
144        return $block_patterns;
145    }
146
147    /**
148     * Returns the list of GutenPen patterns for the current user.
149     *
150     * GutenPen patterns are scoped to the requesting user, so the request must be signed/dispatched
151     * as the current user (see Wpcom_Block_Patterns_Utils::remote_get_as_user). On Simple sites we
152     * additionally short-circuit users who have never used the feature, based on the
153     * `is_gutenpen_user` user attribute, to avoid an unnecessary API call.
154     *
155     * @return array The list of GutenPen patterns, or an empty array when the feature is unavailable to the current user.
156     */
157    private function get_gutenpen_patterns() {
158        // On Simple sites, only users marketed as GutenPen users may have patterns to fetch.
159        if ( defined( 'IS_WPCOM' ) && IS_WPCOM && ! get_user_attribute( get_current_user_id(), 'is_gutenpen_user' ) ) {
160            return array();
161        }
162
163        $cache_key            = sprintf( 'gutenpen_patterns_%d', get_current_user_id() );
164        $override_source_site = apply_filters( 'a8c_override_patterns_source_site', null );
165
166        $patterns      = $this->utils->cache_get( $cache_key, 'ptk_patterns' );
167        $disable_cache = ( function_exists( 'is_automattician' ) && is_automattician() ) || $override_source_site || ( defined( 'WP_DISABLE_PATTERN_CACHE' ) && WP_DISABLE_PATTERN_CACHE );
168
169        if ( $disable_cache || false === $patterns ) {
170            $patterns = $this->utils->remote_get_as_user( '/gutenpen/patterns' );
171
172            if ( ! $disable_cache ) {
173                $this->utils->cache_add( $cache_key, $patterns, 'ptk_patterns', 5 * MINUTE_IN_SECONDS );
174            }
175        }
176
177        return $patterns;
178    }
179
180    /**
181     * Check that the pattern is allowed to be registered.
182     *
183     * Checks for pattern_meta tags with a prefix of `requires-` in the name, and then attempts to match
184     * the remainder of the name to a theme feature.
185     *
186     * For example, to prevent patterns that depend on wide or full-width block alignment support
187     * from being registered in sites where the active theme does not have `align-wide` support,
188     * we can add the `requires-align-wide` pattern_meta tag to the pattern. This function will
189     * then match against that pattern_meta tag, and then return `false`.
190     *
191     * @param array $pattern    A pattern with a 'pattern_meta' array where the key is the tag slug in English.
192     *
193     * @return bool
194     */
195    private function can_register_pattern( $pattern ) {
196        if ( empty( $pattern['pattern_meta'] ) ) {
197            // Default to allowing patterns without metadata to be registered.
198            return true;
199        }
200
201        foreach ( $pattern['pattern_meta'] as $pattern_meta => $value ) {
202            // Match against tags with a non-translated slug beginning with `requires-`.
203            $split_slug = preg_split( '/^requires-/', $pattern_meta );
204
205            // If the theme does not support the matched feature, then skip registering the pattern.
206            if ( isset( $split_slug[1] ) && false === get_theme_support( $split_slug[1] ) ) {
207                return false;
208            }
209        }
210
211        return true;
212    }
213
214    /**
215     * Ensure that all patterns with a blockType property are registered with appropriate postTypes.
216     */
217    private function update_pattern_post_types() {
218        if ( ! class_exists( 'WP_Block_Patterns_Registry' ) ) {
219            return;
220        }
221        foreach ( \WP_Block_Patterns_Registry::get_instance()->get_all_registered() as $pattern ) {
222            if ( array_key_exists( 'postTypes', $pattern ) && $pattern['postTypes'] ) {
223                continue;
224            }
225
226            $post_types = $this->utils->get_pattern_post_types_from_pattern( $pattern );
227            if ( $post_types ) {
228                unregister_block_pattern( $pattern['name'] );
229
230                $pattern['postTypes'] = $post_types;
231                $pattern_name         = $pattern['name'];
232                unset( $pattern['name'] );
233                register_block_pattern( $pattern_name, $pattern );
234            }
235        }
236    }
237}