Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 80
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Post
0.00% covered (danger)
0.00%
0 / 76
0.00% covered (danger)
0.00%
0 / 9
702
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 add_additional_fields_schema
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 create_item
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
42
 extract_terms_ids
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 process_post_meta
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 filter_post_meta_keys
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 get_term_ids_from_slugs
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 create_missing_terms
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 slug_to_readable_name
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Posts REST route
4 *
5 * @package automattic/jetpack-import
6 */
7
8namespace Automattic\Jetpack\Import\Endpoints;
9
10use Automattic\Jetpack\Sync\Settings;
11use WP_Error;
12use WP_REST_Request;
13use WP_REST_Response;
14
15if ( ! defined( 'ABSPATH' ) ) {
16    exit( 0 );
17}
18
19if ( ! function_exists( 'post_exists' ) ) {
20    require_once ABSPATH . 'wp-admin/includes/post.php';
21}
22
23/**
24 * Class Post
25 */
26class Post extends \WP_REST_Posts_Controller {
27
28    /**
29     * Base class
30     */
31    use Import;
32
33    /**
34     * The Import ID add a new item to the schema.
35     */
36    use Import_ID;
37
38    /**
39     * Whether the controller supports batching.
40     *
41     * @var array
42     */
43    protected $allow_batch = array( 'v1' => true );
44
45    /**
46     * Constructor.
47     *
48     * @param string $post_type Post type.
49     */
50    public function __construct( $post_type = 'post' ) {
51        parent::__construct( $post_type );
52
53        // @see add_post_meta
54        $this->import_id_meta_type = $post_type;
55    }
56
57    /**
58     * Adds the schema from additional fields to a schema array.
59     *
60     * The type of object is inferred from the passed schema.
61     *
62     * @param array $schema Schema array.
63     * @return array Modified Schema array.
64     */
65    public function add_additional_fields_schema( $schema ) {
66        // WXR saves terms as slugs, so we need to overwrite the schema.
67        $schema['properties']['categories']['items']['type'] = 'string';
68        $schema['properties']['tags']['items']['type']       = 'string';
69
70        // Add the import unique ID to the schema.
71        return $this->add_unique_identifier_to_schema( $schema );
72    }
73
74    /**
75     * Creates a single post.
76     *
77     * @param WP_REST_Request $request Full details about the request.
78     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
79     */
80    public function create_item( $request ) {
81        // Set the WP_IMPORTING constant to prevent sync notifications
82        $this->set_importing();
83
84        // Skip if the post already exists.
85        $post_id = \post_exists(
86            $request['title'],
87            '',
88            $request['date'],
89            $this->post_type,
90            $request['status']
91        );
92
93        if ( $post_id ) {
94            return new WP_Error(
95                'post_exists',
96                __( 'Cannot create existing post.', 'jetpack-import' ),
97                array(
98                    'status'  => 409,
99                    'post_id' => $post_id,
100                )
101            );
102        }
103
104        // WXR saves terms as slugs, so we need to convert them to IDs before send the data to the legacy endpoint.
105        foreach ( array( 'categories', 'tags' ) as $taxonomy ) {
106            $request[ $taxonomy ] = $this->extract_terms_ids( $request, $taxonomy );
107        }
108
109        $response = parent::create_item( $request );
110
111        // Process post metadata.
112        if ( ! is_wp_error( $response ) && isset( $response->data ) && isset( $response->data['id'] ) ) {
113            $this->process_post_meta( $response->data['id'], $request );
114        }
115
116        return $this->add_import_id_metadata( $request, $response );
117    }
118
119    /**
120     * Extract terms IDs from slugs.
121     *
122     * @param WP_REST_Request $request Full details about the request.
123     * @param string          $taxonomy Taxonomy name.
124     * @return array List of terms IDs.
125     */
126    protected function extract_terms_ids( $request, $taxonomy ) {
127        $ret = is_array( $request[ $taxonomy ] ) ? $request[ $taxonomy ] : array();
128
129        if ( ! count( $ret ) ) {
130            return $ret;
131        }
132
133        $taxonomy_name = $taxonomy === 'tags' ? 'post_tag' : 'category';
134
135        // Extract the terms by ID.
136        $ids = $this->get_term_ids_from_slugs( $ret, $taxonomy_name );
137
138        // Create missing terms and add their IDs to the $ids array.
139        $ids = $this->create_missing_terms( $ret, $ids, $taxonomy_name );
140
141        if ( is_array( $ids ) ) {
142            return $ids;
143        } else {
144            // Flush away any invalid terms.
145            return array();
146        }
147    }
148
149    /**
150     * Processes the metadata of a WordPress post when creating it.
151     *
152     * @param int   $post_id The post ID.
153     * @param mixed $request An object containing the metadata being added to the post.
154     * @return void
155     */
156    public function process_post_meta( $post_id, $request ) {
157        $metas = $request->get_param( 'meta' );
158
159        if ( empty( $metas ) ) {
160            return;
161        }
162
163        $meta_keys_array = $this->filter_post_meta_keys( $metas );
164        // Adding it to the whitelist
165        Settings::update_settings( array( 'post_meta_whitelist' => $meta_keys_array ) );
166
167        if ( is_array( $metas ) ) {
168            foreach ( $metas as $meta_key => $meta_value ) {
169
170                $meta_value = maybe_unserialize( $meta_value );
171                if ( $meta_key === '_edit_last' ) {
172                    update_post_meta( $post_id, $meta_key, $meta_value );
173                } else {
174                    // Add the meta data to the post
175                    add_post_meta( $post_id, $meta_key, $meta_value );
176                }
177
178                do_action( 'import_post_meta', $post_id, $meta_key, $meta_value );
179            }
180        }
181    }
182
183    /**
184     * Filters an array of post meta keys.
185     *
186     * @param array $metas An array of metas to filter.
187     * @return array The filtered array of meta keys.
188     */
189    private function filter_post_meta_keys( $metas ) {
190        // Convert array of keys to a plain array of key strings
191        $meta_keys = array_unique( array_keys( $metas ) );
192        // // Filter the array by removing the excluded keys and any keys that include '_oembed'
193        $filtered_keys = array_filter(
194            $meta_keys,
195            function ( $key ) {
196                // We also don't want to include any oembed post meta because it gets created after a post created
197                return ! str_contains( $key, '_oembed' );
198            }
199        );
200        // Return the filtered array
201        return $filtered_keys;
202    }
203
204    /**
205     * Get term IDs from slugs.
206     *
207     * @param array  $term_slugs      Array of term slugs.
208     * @param string $taxonomy_name   Taxonomy name.
209     *
210     * @return array                  Array of term IDs.
211     */
212    protected function get_term_ids_from_slugs( $term_slugs, $taxonomy_name ) {
213        // @phan-suppress-next-line PhanAccessMethodInternal @phan-suppress-current-line UnusedSuppression -- Fixed in WP 6.9, but then we need a suppression for the WP 6.8 compat run. @todo Remove this suppression when we drop WP <6.9.
214        return get_terms(
215            array(
216                'fields'     => 'ids',
217                'hide_empty' => false,
218                'slug'       => $term_slugs,
219                'taxonomy'   => $taxonomy_name,
220            )
221        );
222    }
223
224    /**
225     * Create any missing terms in the given taxonomy.
226     *
227     * @param array  $term_slugs   The slugs of the terms to check for.
228     * @param array  $existing_ids The IDs of any terms that already exist.
229     * @param string $taxonomy_name The name of the taxonomy.
230     *
231     * @return array The IDs of any terms that are now in the taxonomy.
232     */
233    protected function create_missing_terms( $term_slugs, $existing_ids, $taxonomy_name ) {
234        $ids = $existing_ids;
235
236        foreach ( $term_slugs as $term_slug ) {
237            if ( ! term_exists( $term_slug, $taxonomy_name ) ) {
238                $term_name = $this->slug_to_readable_name( $term_slug );
239                $new_term  = wp_insert_term( $term_name, $taxonomy_name, array( 'slug' => $term_slug ) );
240                if ( ! is_wp_error( $new_term ) && isset( $new_term['term_id'] ) ) {
241                    $ids[] = $new_term['term_id'];
242                }
243            }
244        }
245
246        return $ids;
247    }
248
249    /**
250     * Convert a slug to a readable name.
251     *
252     * @param string $slug Slug to convert.
253     * @return string Converted name.
254     */
255    protected function slug_to_readable_name( $slug ) {
256        $name = str_replace( array( '-', '_' ), ' ', $slug );
257        $name = ucwords( $name );
258        return $name;
259    }
260}