Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 209
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
WP_Template_Inserter
0.00% covered (danger)
0.00%
0 / 209
0.00% covered (danger)
0.00%
0 / 8
650
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 fetch_template_parts
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
72
 fetch_retry
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 get_default_header
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_default_footer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_template_data_inserted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 insert_default_template_data
0.00% covered (danger)
0.00%
0 / 62
0.00% covered (danger)
0.00%
0 / 1
72
 register_template_post_types
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Full site editing file.
4 *
5 * @package A8C\FSE
6 */
7
8namespace Automattic\Jetpack\Jetpack_Mu_Wpcom\Wpcom_Legacy_FSE;
9
10/**
11 * Class WP_Template_Inserter
12 */
13class WP_Template_Inserter {
14    /**
15     * Template header content.
16     *
17     * @var string $header_content
18     */
19    private $header_content;
20
21    /**
22     * Template footer content.
23     *
24     * @var string $footer_content
25     */
26    private $footer_content;
27
28    /**
29     * Current theme slug.
30     *
31     * @var string $theme_slug
32     */
33    private $theme_slug;
34
35    /**
36     * Image URLs contained in the returned from the template API
37     *
38     * @var array $image_urls
39     */
40    private $image_urls;
41
42    /**
43     * This site option will be used to indicate that template data has already been
44     * inserted for this theme, in order to prevent this functionality from running
45     * more than once.
46     *
47     * @var string $fse_template_data_option
48     */
49    private $fse_template_data_option;
50
51    /**
52     * The strategy to use for default data insertion.
53     *
54     * 'use-api' will use the wpcom API to get specifc content depending on the theme.
55     *
56     * 'use-local' will use the locally defined defaults.
57     *
58     * @var string $loading_strategy
59     */
60    private $loading_strategy;
61
62    /**
63     * WP_Template_Inserter constructor.
64     *
65     * @param string $theme_slug Current theme slug.
66     * @param string $loading_strategy The strategy to use to load the template part content.
67     */
68    public function __construct( $theme_slug, $loading_strategy = 'use-local' ) {
69        $this->theme_slug       = $theme_slug;
70        $this->header_content   = '';
71        $this->footer_content   = '';
72        $this->loading_strategy = $loading_strategy;
73
74        /*
75         * Previously the option suffix was '-fse-template-data'. Bumping this to '-fse-template-data-v1'
76         * to differentiate it from the old data that was not provided by the API. Note that we don't want
77         * to tie this to plugin version constant, because that would trigger the insertion on each plugin
78         * update, even when it's not necessary (it would duplicate existing data).
79         */
80        $this->fse_template_data_option = $this->theme_slug . '-fse-template-data-v1';
81    }
82
83    /**
84     * Retrieves template parts content.
85     */
86    public function fetch_template_parts() {
87        // Use default data if we don't want to fetch from the API.
88        if ( 'use-local' === $this->loading_strategy ) {
89            $this->header_content = $this->get_default_header();
90            $this->footer_content = $this->get_default_footer();
91            return;
92        }
93
94        $request_url = 'https://public-api.wordpress.com/wpcom/v2/full-site-editing/templates';
95
96        $request_args = array(
97            'body' => array( 'theme_slug' => $this->theme_slug ),
98        );
99
100        $response = $this->fetch_retry( $request_url, $request_args );
101
102        if ( ! $response ) {
103            do_action(
104                'a8c_fse_log',
105                'template_population_failure',
106                array(
107                    'context'    => 'WP_Template_Inserter->fetch_template_parts',
108                    'error'      => 'Fetch retry timeout',
109                    'theme_slug' => $this->theme_slug,
110                )
111            );
112            $this->header_content = $this->get_default_header();
113            $this->footer_content = $this->get_default_footer();
114            return;
115        }
116
117        $api_response = json_decode( wp_remote_retrieve_body( $response ), true );
118        if ( ! empty( $api_response['code'] ) && 'not_found' === $api_response['code'] ) {
119            do_action(
120                'a8c_fse_log',
121                'template_population_failure',
122                array(
123                    'context'    => 'WP_Template_Inserter->fetch_template_parts',
124                    'error'      => 'Did not find remote template data for the given theme.',
125                    'theme_slug' => $this->theme_slug,
126                )
127            );
128            return;
129        }
130
131        // Default to first returned header for now. Support for multiple headers will be added in future iterations.
132        if ( ! empty( $api_response['headers'] ) ) {
133            $this->header_content = $api_response['headers'][0];
134        }
135
136        // Default to first returned footer for now. Support for multiple footers will be added in future iterations.
137        if ( ! empty( $api_response['footers'] ) ) {
138            $this->footer_content = $api_response['footers'][0];
139        }
140
141        // This should contain all image URLs for images in any header or footer.
142        if ( ! empty( $api_response['image_urls'] ) ) {
143            $this->image_urls = $api_response['image_urls'];
144        }
145    }
146
147    /**
148     * Retries a call to wp_remote_get on error.
149     *
150     * @param string $request_url Url of the api call to make.
151     * @param array  $request_args Additional arguments for the api call.
152     * @param int    $attempt The number of the attempt being made.
153     * @return array|null wp_remote_get response array
154     */
155    private function fetch_retry( $request_url, $request_args = null, $attempt = 1 ) {
156        $max_retries = 3;
157
158        $response = wp_remote_get( $request_url, $request_args );
159
160        if ( ! is_wp_error( $response ) ) {
161            return $response;
162        }
163
164        if ( $attempt > $max_retries ) {
165            return null;
166        }
167
168        sleep( pow( 2, $attempt ) );
169        ++$attempt;
170        return $this->fetch_retry( $request_url, $request_args, $attempt );
171    }
172
173    /**
174     * Returns a default header if call to template api fails for some reason.
175     *
176     * @return string Content of a default header
177     */
178    public function get_default_header() {
179        return '<!-- wp:a8c/site-description /-->
180            <!-- wp:a8c/site-title /-->
181            <!-- wp:a8c/navigation-menu /-->';
182    }
183
184    /**
185     * Returns a default footer if call to template api fails for some reason.
186     *
187     * @return string Content of a default footer
188     */
189    public function get_default_footer() {
190        return '<!-- wp:a8c/navigation-menu /-->';
191    }
192
193    /**
194     * Determines whether FSE data has already been inserted.
195     *
196     * @return bool True if FSE data has already been inserted, false otherwise.
197     */
198    public function is_template_data_inserted() {
199        return get_option( $this->fse_template_data_option ) ? true : false;
200    }
201
202    /**
203     * This function will be called on plugin activation hook.
204     */
205    public function insert_default_template_data() {
206        do_action(
207            'a8c_fse_log',
208            'before_template_population',
209            array(
210                'context'    => 'WP_Template_Inserter->insert_default_template_data',
211                'theme_slug' => $this->theme_slug,
212            )
213        );
214
215        if ( $this->is_template_data_inserted() ) {
216            /*
217             * Bail here to prevent inserting the FSE data twice for any given theme.
218             * Multiple themes will still be able to insert different templates.
219             */
220            do_action(
221                'a8c_fse_log',
222                'template_population_failure',
223                array(
224                    'context'    => 'WP_Template_Inserter->insert_default_template_data',
225                    'error'      => 'Data already exist',
226                    'theme_slug' => $this->theme_slug,
227                )
228            );
229            return;
230        }
231
232        // Set header and footer content based on data fetched from the WP.com API.
233        $this->fetch_template_parts();
234
235        // Avoid creating template parts if data hasn't been fetched properly.
236        if ( empty( $this->header_content ) || empty( $this->footer_content ) ) {
237            return;
238        }
239
240        $this->register_template_post_types();
241
242        $header_id = wp_insert_post(
243            array(
244                'post_title'     => 'Header',
245                'post_content'   => $this->header_content,
246                'post_status'    => 'publish',
247                'post_type'      => 'wp_template_part',
248                'comment_status' => 'closed',
249                'ping_status'    => 'closed',
250            )
251        );
252
253        if ( ! term_exists( "$this->theme_slug-header", 'wp_template_part_type' ) ) {
254            wp_insert_term( "$this->theme_slug-header", 'wp_template_part_type' );
255        }
256
257        wp_set_object_terms( $header_id, "$this->theme_slug-header", 'wp_template_part_type' );
258
259        $footer_id = wp_insert_post(
260            array(
261                'post_title'     => 'Footer',
262                'post_content'   => $this->footer_content,
263                'post_status'    => 'publish',
264                'post_type'      => 'wp_template_part',
265                'comment_status' => 'closed',
266                'ping_status'    => 'closed',
267            )
268        );
269
270        if ( ! term_exists( "$this->theme_slug-footer", 'wp_template_part_type' ) ) {
271            wp_insert_term( "$this->theme_slug-footer", 'wp_template_part_type' );
272        }
273
274        wp_set_object_terms( $footer_id, "$this->theme_slug-footer", 'wp_template_part_type' );
275
276        add_option( $this->fse_template_data_option, true );
277
278        // Note: we set the option before doing the image upload because the template
279        // parts can work with the remote URLs even if this fails.
280        $image_urls = $this->image_urls;
281        if ( ! empty( $image_urls ) ) {
282            // Uploading images locally does not work in the WordPress.com environment,
283            // so we use an action to handle it with Headstart there.
284            if ( has_action( 'a8c_fse_upload_template_part_images' ) ) {
285                do_action( 'a8c_fse_upload_template_part_images', $image_urls, array( $header_id, $footer_id ) );
286            }
287        }
288
289        do_action(
290            'a8c_fse_log',
291            'template_population_success',
292            array(
293                'context'    => 'WP_Template_Inserter->insert_default_template_data',
294                'theme_slug' => $this->theme_slug,
295            )
296        );
297    }
298
299    /**
300     * Register post types.
301     */
302    public function register_template_post_types() {
303        register_post_type(
304            'wp_template_part', // phpcs:ignore WordPress.NamingConventions.ValidPostTypeSlug.Reserved
305            array(
306                'labels'          => array(
307                    'name'                     => _x( 'Template Parts', 'post type general name', 'jetpack-mu-wpcom' ),
308                    'singular_name'            => _x( 'Template Part', 'post type singular name', 'jetpack-mu-wpcom' ),
309                    'menu_name'                => _x( 'Template Parts', 'admin menu', 'jetpack-mu-wpcom' ),
310                    'name_admin_bar'           => _x( 'Template Part', 'add new on admin bar', 'jetpack-mu-wpcom' ),
311                    'add_new'                  => _x( 'Add New', 'Template', 'jetpack-mu-wpcom' ),
312                    'add_new_item'             => __( 'Add New Template Part', 'jetpack-mu-wpcom' ),
313                    'new_item'                 => __( 'New Template Part', 'jetpack-mu-wpcom' ),
314                    'edit_item'                => __( 'Edit Template Part', 'jetpack-mu-wpcom' ),
315                    'view_item'                => __( 'View Template Part', 'jetpack-mu-wpcom' ),
316                    'all_items'                => __( 'All Template Parts', 'jetpack-mu-wpcom' ),
317                    'search_items'             => __( 'Search Template Parts', 'jetpack-mu-wpcom' ),
318                    'not_found'                => __( 'No template parts found.', 'jetpack-mu-wpcom' ),
319                    'not_found_in_trash'       => __( 'No template parts found in Trash.', 'jetpack-mu-wpcom' ),
320                    'filter_items_list'        => __( 'Filter template parts list', 'jetpack-mu-wpcom' ),
321                    'items_list_navigation'    => __( 'Template parts list navigation', 'jetpack-mu-wpcom' ),
322                    'items_list'               => __( 'Template parts list', 'jetpack-mu-wpcom' ),
323                    'item_published'           => __( 'Template part published.', 'jetpack-mu-wpcom' ),
324                    'item_published_privately' => __( 'Template part published privately.', 'jetpack-mu-wpcom' ),
325                    'item_reverted_to_draft'   => __( 'Template part reverted to draft.', 'jetpack-mu-wpcom' ),
326                    'item_scheduled'           => __( 'Template part scheduled.', 'jetpack-mu-wpcom' ),
327                    'item_updated'             => __( 'Template part updated.', 'jetpack-mu-wpcom' ),
328                ),
329                'menu_icon'       => 'dashicons-layout',
330                'public'          => false,
331                'show_ui'         => true, // Otherwise we'd get permission error when trying to edit them.
332                'show_in_menu'    => false,
333                'rewrite'         => false,
334                'capability_type' => 'template_part',
335                'capabilities'    => array(
336                    // You need to be able to edit posts, in order to read templates in their raw form.
337                    'read'                   => 'edit_posts',
338                    // You need to be able to customize, in order to create templates.
339                    'create_posts'           => 'edit_theme_options',
340                    'edit_posts'             => 'edit_theme_options',
341                    'delete_posts'           => 'edit_theme_options',
342                    'edit_published_posts'   => 'edit_theme_options',
343                    'delete_published_posts' => 'edit_theme_options',
344                    'edit_others_posts'      => 'edit_theme_options',
345                    'delete_others_posts'    => 'edit_theme_options',
346                    'publish_posts'          => 'edit_theme_options',
347                ),
348                'map_meta_cap'    => true,
349                'supports'        => array(
350                    'title',
351                    'editor',
352                    'revisions',
353                ),
354            )
355        );
356
357        register_taxonomy(
358            'wp_template_part_type',
359            'wp_template_part',
360            array(
361                'labels'             => array(
362                    'name'              => _x( 'Template Part Types', 'taxonomy general name', 'jetpack-mu-wpcom' ),
363                    'singular_name'     => _x( 'Template Part Type', 'taxonomy singular name', 'jetpack-mu-wpcom' ),
364                    'menu_name'         => _x( 'Template Part Types', 'admin menu', 'jetpack-mu-wpcom' ),
365                    'all_items'         => __( 'All Template Part Types', 'jetpack-mu-wpcom' ),
366                    'edit_item'         => __( 'Edit Template Part Type', 'jetpack-mu-wpcom' ),
367                    'view_item'         => __( 'View Template Part Type', 'jetpack-mu-wpcom' ),
368                    'update_item'       => __( 'Update Template Part Type', 'jetpack-mu-wpcom' ),
369                    'add_new_item'      => __( 'Add New Template Part Type', 'jetpack-mu-wpcom' ),
370                    'new_item_name'     => __( 'New Template Part Type', 'jetpack-mu-wpcom' ),
371                    'parent_item'       => __( 'Parent Template Part Type', 'jetpack-mu-wpcom' ),
372                    'parent_item_colon' => __( 'Parent Template Part Type:', 'jetpack-mu-wpcom' ),
373                    'search_items'      => __( 'Search Template Part Types', 'jetpack-mu-wpcom' ),
374                    'not_found'         => __( 'No template part types found.', 'jetpack-mu-wpcom' ),
375                    'back_to_items'     => __( 'Back to template part types', 'jetpack-mu-wpcom' ),
376                ),
377                'public'             => false,
378                'publicly_queryable' => false,
379                'show_ui'            => false,
380                'show_in_menu'       => false,
381                'show_in_nav_menu'   => false,
382                'show_in_rest'       => true,
383                'rest_base'          => 'template_part_types',
384                'show_tagcloud'      => false,
385                'hierarchical'       => true,
386                'rewrite'            => false,
387                'capabilities'       => array(
388                    'manage_terms' => 'edit_theme_options',
389                    'edit_terms'   => 'edit_theme_options',
390                    'delete_terms' => 'edit_theme_options',
391                    'assign_terms' => 'edit_theme_options',
392                ),
393            )
394        );
395    }
396}