Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.77% covered (warning)
81.77%
157 / 192
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_JSON_API_Post_v1_1_Endpoint
82.63% covered (warning)
82.63%
157 / 190
16.67% covered (danger)
16.67%
1 / 6
106.26
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 get_post_by
72.73% covered (warning)
72.73%
16 / 22
0.00% covered (danger)
0.00%
0 / 1
10.64
 get_sal_post_by
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 render_response_keys
99.25% covered (success)
99.25%
132 / 133
0.00% covered (danger)
0.00%
0 / 1
45
 filter_response
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
182
 get_blog_post
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
1<?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3if ( ! defined( 'ABSPATH' ) ) {
4    exit( 0 );
5}
6
7/**
8 * Post v1_1 Endpoint class.
9 */
10abstract class WPCOM_JSON_API_Post_v1_1_Endpoint extends WPCOM_JSON_API_Endpoint { // phpcs:ignore PEAR.NamingConventions.ValidClassName.Invalid, Generic.Classes.OpeningBraceSameLine.ContentAfterBrace
11    /**
12     * Post object format.
13     *
14     * @var array
15     */
16    public $post_object_format = array(
17        // explicitly document and cast all output
18        'ID'               => '(int) The post ID.',
19        'site_ID'          => '(int) The site ID.',
20        'author'           => '(object>author) The author of the post.',
21        'date'             => "(ISO 8601 datetime) The post's creation time.",
22        'modified'         => "(ISO 8601 datetime) The post's most recent update time.",
23        'title'            => '(HTML) <code>context</code> dependent.',
24        'URL'              => '(URL) The full permalink URL to the post.',
25        'short_URL'        => '(URL) The wp.me short URL.',
26        'content'          => '(HTML) <code>context</code> dependent.',
27        'excerpt'          => '(HTML) <code>context</code> dependent.',
28        'slug'             => '(string) The name (slug) for the post, used in URLs.',
29        'guid'             => '(string) The GUID for the post.',
30        'status'           => array(
31            'publish'    => 'The post is published.',
32            'draft'      => 'The post is saved as a draft.',
33            'pending'    => 'The post is pending editorial approval.',
34            'private'    => 'The post is published privately',
35            'future'     => 'The post is scheduled for future publishing.',
36            'trash'      => 'The post is in the trash.',
37            'auto-draft' => 'The post is a placeholder for a new post.',
38        ),
39        'sticky'           => '(bool) Is the post sticky?',
40        'password'         => '(string) The plaintext password protecting the post, or, more likely, the empty string if the post is not password protected.',
41        'has_password'     => '(bool) Whether the post is password protected, regardless of whether the current user can access it.',
42        'parent'           => "(object>post_reference|false) A reference to the post's parent, if it has one.",
43        'type'             => "(string) The post's post_type. Post types besides post, page and revision need to be whitelisted using the <code>rest_api_allowed_post_types</code> filter.",
44        'discussion'       => '(object) Hash of discussion options for the post',
45        'likes_enabled'    => '(bool) Is the post open to likes?',
46        'sharing_enabled'  => '(bool) Should sharing buttons show on this post?',
47        'like_count'       => '(int) The number of likes for this post.',
48        'i_like'           => '(bool) Does the current user like this post?',
49        'is_reblogged'     => '(bool) Did the current user reblog this post?',
50        'is_following'     => '(bool) Is the current user following this blog?',
51        'global_ID'        => '(string) A unique WordPress.com-wide representation of a post.',
52        'featured_image'   => '(URL) The URL to the featured image for this post if it has one.',
53        'post_thumbnail'   => '(object>attachment) The attachment object for the featured image if it has one.',
54        'format'           => array(), // see constructor
55        'geo'              => '(object>geo|false)',
56        'menu_order'       => '(int) (Pages Only) The order pages should appear in.',
57        'page_template'    => '(string) (Pages Only) The page template this page is using.',
58        'publicize_URLs'   => '(array:URL) Array of Facebook URLs published by this post.',
59        'terms'            => '(object) Hash of taxonomy names mapping to a hash of terms keyed by term name.',
60        'tags'             => '(object:tag) Hash of tags (keyed by tag name) applied to the post.',
61        'categories'       => '(object:category) Hash of categories (keyed by category name) applied to the post.',
62        'attachments'      => '(object:attachment) Hash of post attachments (keyed by attachment ID). Returns the most recent 20 attachments. Use the `/sites/$site/media` endpoint to query the attachments beyond the default of 20 that are returned here.',
63        'attachment_count' => '(int) The total number of attachments for this post. Use the `/sites/$site/media` endpoint to query the attachments beyond the default of 20 that are returned here.',
64        'metadata'         => '(array) Array of post metadata keys and values. All unprotected meta keys are available by default for read requests. Both unprotected and protected meta keys are available for authenticated requests with access. Protected meta keys can be made available with the <code>rest_api_allowed_public_metadata</code> filter.',
65        'meta'             => '(object) API result meta data',
66        'capabilities'     => '(object) List of post-specific permissions for the user; publish_post, edit_post, delete_post',
67        'revisions'        => '(array) List of post revision IDs. Only available for posts retrieved with context=edit.',
68        'other_URLs'       => '(object) List of URLs for this post. Permalink and slug suggestions.',
69    );
70
71    /**
72     * Constructor function.
73     *
74     * @param string|array|object $args — Args.
75     */
76    public function __construct( $args ) {
77        if ( is_array( $this->post_object_format ) && isset( $this->post_object_format['format'] ) ) {
78            $this->post_object_format['format'] = get_post_format_strings();
79        }
80        if ( ! $this->response_format ) {
81            $this->response_format =& $this->post_object_format;
82        }
83        parent::__construct( $args );
84    }
85
86    /**
87     * Get a post by a specified field and value
88     *
89     * @param string $field - the field.
90     * @param string $field_value - the field value.
91     * @param string $context Post use context (e.g. 'display').
92     *
93     * @return array|WP_Error Post
94     **/
95    public function get_post_by( $field, $field_value, $context = 'display' ) {
96
97        // validate input
98        if ( ! in_array( $field, array( 'ID', 'name' ), true ) ) {
99            return new WP_Error( 'invalid_field', 'Invalid API FIELD', 400 );
100        }
101
102        if ( ! in_array( $context, array( 'display', 'edit' ), true ) ) {
103            return new WP_Error( 'invalid_context', 'Invalid API CONTEXT', 400 );
104        }
105
106        if ( 'display' === $context ) {
107            $args = $this->query_args();
108            if ( isset( $args['content_width'] ) && $args['content_width'] ) {
109                $GLOBALS['content_width'] = (int) $args['content_width'];
110            }
111        }
112
113        // fetch SAL post
114        $post = $this->get_sal_post_by( $field, $field_value, $context );
115
116        if ( is_wp_error( $post ) ) {
117            return $post;
118        }
119
120        $GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
121
122        // TODO: not sure where this one should go
123        if ( 'display' === $context ) {
124            setup_postdata( $post );
125        }
126
127        $keys_to_render = array_keys( $this->post_object_format );
128        if ( isset( $this->api->query['fields'] ) ) {
129            $limit_to_fields = array_map( 'trim', explode( ',', $this->api->query['fields'] ) );
130            $keys_to_render  = array_intersect( $keys_to_render, $limit_to_fields );
131        }
132
133        // always include some keys because processors require it to validate access
134        $keys_to_render = array_unique( array_merge( $keys_to_render, array( 'type', 'status', 'password' ) ) );
135
136        $response = $this->render_response_keys( $post, $context, $keys_to_render );
137
138        unset( $GLOBALS['post'] );
139
140        return $response;
141    }
142
143    /**
144     * Get SAL post by a specified field and value
145     *
146     * @param string $field - the field.
147     * @param string $field_value - the field value.
148     * @param string $context Post use context (e.g. 'display').
149     *
150     * @return SAL_Post|WP_Error Post
151     **/
152    protected function get_sal_post_by( $field, $field_value, $context ) {
153        global $blog_id;
154
155        $site = $this->get_platform()->get_site( $blog_id );
156
157        $post = ( $field === 'name' ) ?
158            $site->get_post_by_name( $field_value, $context ) :
159            $site->get_post_by_id( $field_value, $context );
160
161        return $post;
162    }
163
164    /**
165     * Render the response keys.
166     *
167     * @param object $post - the post.
168     * @param string $context Post use context (e.g. 'display').
169     * @param array  $keys - the keys.
170     *
171     * @return array
172     */
173    private function render_response_keys( $post, $context, $keys ) {
174        $response = array();
175        foreach ( $keys as $key ) {
176            switch ( $key ) {
177                case 'ID':
178                    // explicitly cast all output
179                    $response[ $key ] = (int) $post->ID;
180                    break;
181                case 'site_ID':
182                    $response[ $key ] = $post->site->get_id();
183                    break;
184                case 'author':
185                    $response[ $key ] = $post->get_author();
186                    break;
187                case 'date':
188                    $response[ $key ] = $post->get_date();
189                    break;
190                case 'modified':
191                    $response[ $key ] = $post->get_modified_date();
192                    break;
193                case 'title':
194                    $response[ $key ] = $post->get_title();
195                    break;
196                case 'URL':
197                    $response[ $key ] = $post->get_url();
198                    break;
199                case 'short_URL':
200                    $response[ $key ] = $post->get_shortlink();
201                    break;
202                case 'content':
203                    $response[ $key ] = $post->get_content();
204                    break;
205                case 'excerpt':
206                    $response[ $key ] = $post->get_excerpt();
207                    break;
208                case 'status':
209                    $response[ $key ] = $post->get_status();
210                    break;
211                case 'sticky':
212                    $response[ $key ] = $post->is_sticky();
213                    break;
214                case 'slug':
215                    $response[ $key ] = $post->get_slug();
216                    break;
217                case 'guid':
218                    $response[ $key ] = $post->get_guid();
219                    break;
220                case 'password':
221                    $response[ $key ] = $post->get_password();
222                    break;
223                case 'has_password':
224                    $response[ $key ] = $post->get_has_password();
225                    break;
226                /** (object|false) */
227                case 'parent':
228                    $response[ $key ] = $post->get_parent();
229                    break;
230                case 'type':
231                    $response[ $key ] = $post->get_type();
232                    break;
233                case 'discussion':
234                    $response[ $key ] = $post->get_discussion();
235                    break;
236                case 'likes_enabled':
237                    $response[ $key ] = $post->is_likes_enabled();
238                    break;
239                case 'sharing_enabled':
240                    $response[ $key ] = $post->is_sharing_enabled();
241                    break;
242                case 'like_count':
243                    $response[ $key ] = $post->get_like_count();
244                    break;
245                case 'i_like':
246                    $response[ $key ] = $post->is_liked();
247                    break;
248                case 'is_reblogged':
249                    $response[ $key ] = $post->is_reblogged();
250                    break;
251                case 'is_following':
252                    $response[ $key ] = $post->is_following();
253                    break;
254                case 'global_ID':
255                    $response[ $key ] = $post->get_global_id();
256                    break;
257                case 'featured_image':
258                    $response[ $key ] = $post->get_featured_image();
259                    break;
260                case 'post_thumbnail':
261                    $response[ $key ] = $post->get_post_thumbnail();
262                    break;
263                case 'format':
264                    $response[ $key ] = $post->get_format();
265                    break;
266                /** (object|false) */
267                case 'geo':
268                    $response[ $key ] = $post->get_geo();
269                    break;
270                case 'menu_order':
271                    $response[ $key ] = $post->get_menu_order();
272                    break;
273                case 'page_template':
274                    $response[ $key ] = $post->get_page_template();
275                    break;
276                case 'publicize_URLs':
277                    $response[ $key ] = $post->get_publicize_urls();
278                    break;
279                case 'terms':
280                    $response[ $key ] = $post->get_terms();
281                    break;
282                case 'tags':
283                    $response[ $key ] = $post->get_tags();
284                    break;
285                case 'categories':
286                    $response[ $key ] = $post->get_categories();
287                    break;
288                case 'attachments':
289                    list( $attachments, $attachment_count ) = $post->get_attachments_and_count();
290                    $response[ $key ]                       = $attachments;
291                    $response['attachment_count']           = $attachment_count;
292                    break;
293                /** (array|false) */
294                case 'metadata':
295                    $response[ $key ] = $post->get_metadata();
296                    break;
297                case 'meta':
298                    $response[ $key ] = $post->get_meta();
299                    break;
300                case 'capabilities':
301                    $response[ $key ] = $post->get_current_user_capabilities();
302                    break;
303                case 'revisions':
304                    $revisions = $post->get_revisions();
305                    if ( $revisions ) {
306                        $response[ $key ] = $revisions;
307                    }
308                    break;
309                case 'other_URLs':
310                    $response[ $key ] = $post->get_other_urls();
311                    break;
312            }
313        }
314
315        return $response;
316    }
317
318    /**
319     * Filter respnse.
320     *
321     * @param array $response - the response.
322     * @return array Filtered response.
323     */
324    public function filter_response( $response ) {
325
326        // Do minimal processing if the caller didn't request it
327        if ( ! isset( $_REQUEST['meta_fields'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- we're not making any changes to the site.
328            return $response;
329        }
330
331        // Bail early if we do not have the necessary data.
332        if (
333            is_wp_error( $response )
334            || ! isset( $response['posts'] )
335            || ! is_array( $response['posts'] )
336        ) {
337            return $response;
338        }
339
340        // Retrieve an array of field paths, such as: [`autosave.modified`, `autosave.post_ID`]
341        $fields = explode( ',', sanitize_text_field( wp_unslash( $_REQUEST['meta_fields'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- we're not making any changes to the site.
342
343        foreach ( $response['posts'] as $post ) {
344
345            if ( ! isset( $post['meta'] ) || ! isset( $post['meta']->data ) || ( ! is_array( $post['meta']->data ) && ! is_object( $post['meta']->data ) ) ) {
346                continue;
347            }
348
349            $newmeta = array();
350            foreach ( $post['meta']->data as $field_key => $field_value ) {
351
352                foreach ( $field_value as $subfield_key => $subfield_value ) {
353                    $key_path = $field_key . '.' . $subfield_key;
354
355                    if ( in_array( $key_path, $fields, true ) ) {
356                        $newmeta[ $field_key ][ $subfield_key ] = $subfield_value;
357                    }
358                }
359            }
360
361            $post['meta']->data = $newmeta;
362        }
363
364        return $response;
365    }
366
367    /**
368     * Gets the blog post.
369     *
370     * @param int    $blog_id - the blog ID.
371     * @param int    $post_id - the post ID.
372     * @param string $context - the context.
373     *
374     * @return array|bool|WP_Error
375     */
376    public function get_blog_post( $blog_id, $post_id, $context = 'display' ) {
377        $blog_id = $this->api->get_blog_id( $blog_id );
378        if ( ! $blog_id || is_wp_error( $blog_id ) ) {
379            return $blog_id;
380        }
381        switch_to_blog( $blog_id );
382        $post = $this->get_post_by( 'ID', $post_id, $context );
383        restore_current_blog();
384        return $post;
385    }
386}