Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 156
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
WP_REST_Newspack_Articles_Controller
0.00% covered (danger)
0.00%
0 / 156
0.00% covered (danger)
0.00%
0 / 6
210
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 register_routes
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
2
 get_items
0.00% covered (danger)
0.00%
0 / 61
0.00% covered (danger)
0.00%
0 / 1
72
 articles_response
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 filter_viewable_post_types
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_attribute_schema
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * WP_REST_Newspack_Articles_Controller file.
4 *
5 * @package WordPress
6 */
7
8// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound
9/**
10 * Class WP_REST_Newspack_Articles_Controller.
11 */
12class WP_REST_Newspack_Articles_Controller extends WP_REST_Controller {
13// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound
14
15    /**
16     * Attribute schema.
17     *
18     * @var array
19     */
20    public $attribute_schema;
21
22    /**
23     * Constructs the controller.
24     *
25     * @access public
26     */
27    public function __construct() {
28        $this->namespace = 'newspack-blocks/v1';
29    }
30
31    /**
32     * Registers the necessary REST API routes.
33     *
34     * @access public
35     */
36    public function register_routes() {
37        // Endpoint to get articles on the front-end.
38        register_rest_route(
39            $this->namespace,
40            '/articles',
41            [
42                [
43                    'methods'             => WP_REST_Server::READABLE,
44                    'callback'            => [ $this, 'get_items' ],
45                    'args'                => $this->get_attribute_schema(),
46                    'permission_callback' => '__return_true',
47                ],
48            ]
49        );
50
51        // Endpoint to get articles in the editor, in regular/query mode.
52        register_rest_route(
53            $this->namespace,
54            '/newspack-blocks-posts',
55            [
56                'methods'             => \WP_REST_Server::READABLE,
57                'callback'            => [ 'Newspack_Blocks_API', 'posts_endpoint' ],
58                'args'                => array_merge(
59                    $this->get_attribute_schema(),
60                    [
61                        'exclude' => [ // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
62                            'type'    => 'array',
63                            'items'   => array(
64                                'type' => 'integer',
65                            ),
66                            'default' => array(),
67                        ],
68                        'include' => [
69                            'type'    => 'array',
70                            'items'   => array(
71                                'type' => 'integer',
72                            ),
73                            'default' => array(),
74                        ],
75                    ]
76                ),
77                'permission_callback' => function() {
78                    return current_user_can( 'edit_posts' );
79                },
80            ]
81        );
82
83        // Endpoint to get articles in the editor, in specific posts mode.
84        register_rest_route(
85            $this->namespace,
86            '/newspack-blocks-specific-posts',
87            [
88                'methods'             => \WP_REST_Server::READABLE,
89                'callback'            => [ 'Newspack_Blocks_API', 'specific_posts_endpoint' ],
90                'args'                => [
91                    'search'      => [
92                        'sanitize_callback' => 'sanitize_text_field',
93                    ],
94                    'postsToShow' => [
95                        'sanitize_callback' => 'absint',
96                    ],
97                    'postType'    => [
98                        'type'    => 'array',
99                        'items'   => array(
100                            'type' => 'string',
101                        ),
102                        'default' => array(),
103                    ],
104                ],
105                'permission_callback' => function() {
106                    return current_user_can( 'edit_posts' );
107                },
108            ]
109        );
110    }
111
112    /**
113     * Returns a list of rendered posts.
114     *
115     * @param WP_REST_Request $request Request object.
116     * @return WP_REST_Response
117     */
118    public function get_items( $request ) {
119        $page        = (int) $request->get_param( 'page' ) ?? 1;
120        $exclude_ids = $request->get_param( 'exclude_ids' ) ?? [];
121        $next_page   = $page + 1;
122        $attributes  = wp_parse_args(
123            $request->get_params() ?? [],
124            wp_list_pluck( $this->get_attribute_schema(), 'default' )
125        );
126
127        $deduplicate = $request->get_param( 'deduplicate' ) ?? 1;
128        if ( ! $deduplicate ) {
129            $exclude_ids = [];
130        }
131
132        // This endpoint is public, so restrict it to publicly viewable post types — matching
133        // what WordPress exposes on the front end. The editor endpoints are capability-gated.
134        $attributes['postType'] = self::filter_viewable_post_types( $attributes['postType'] );
135        if ( empty( $attributes['postType'] ) ) {
136            // Every requested post type was non-viewable; return nothing rather than
137            // substituting a different post type.
138            return self::articles_response();
139        }
140
141        $article_query_args = Newspack_Blocks::build_articles_query( $attributes, apply_filters( 'newspack_blocks_block_name', 'newspack-blocks/homepage-articles' ) );
142
143        // If using exclude_ids, don't worry about pagination. Just get the next postsToShow number of results without the excluded posts. Otherwise, use standard WP pagination.
144        $query = ! empty( $exclude_ids ) ?
145            array_merge(
146                $article_query_args,
147                [
148                    'post__not_in' => $exclude_ids, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
149                ]
150            ) :
151            array_merge(
152                $article_query_args,
153                [
154                    'paged' => $page,
155                ]
156            );
157
158        // Run Query.
159        $article_query = new WP_Query( $query );
160
161        // Defaults.
162        $items    = [];
163        $ids      = [];
164        $next_url = '';
165
166        Newspack_Blocks::filter_excerpt( $attributes );
167
168        // The Loop.
169        while ( $article_query->have_posts() ) {
170            $article_query->the_post();
171            $html = Newspack_Blocks::template_inc(
172                __DIR__ . '/templates/article.php',
173                [
174                    'attributes' => $attributes,
175                ]
176            );
177
178            $items[]['html'] = $html;
179            $ids[]           = get_the_ID();
180        }
181
182        Newspack_Blocks::remove_excerpt_filter();
183
184        // Provide next URL if there are more pages.
185        $show_next_button = ! empty( $exclude_ids ) ? $article_query->max_num_pages > 1 : $article_query->max_num_pages > $next_page;
186        if ( $show_next_button ) {
187            $next_url = add_query_arg(
188                array_merge(
189                    array_map(
190                        function( $attribute ) {
191                            return false === $attribute ? '0' : $attribute;
192                        },
193                        $attributes
194                    ),
195                    [
196                        'exclude_ids' => false,
197                        'page'        => $next_page,
198                    ]
199                ),
200                rest_url( '/newspack-blocks/v1/articles' )
201            );
202        }
203
204        return self::articles_response( $items, $ids, $next_url );
205    }
206
207    /**
208     * Build the articles endpoint response.
209     *
210     * @param array  $items Rendered article items.
211     * @param array  $ids   Post ids in the response.
212     * @param string $next  URL for the next page, or empty string when there is none.
213     * @return WP_REST_Response
214     */
215    private static function articles_response( $items = [], $ids = [], $next = '' ) {
216        return rest_ensure_response(
217            [
218                'items' => $items,
219                'ids'   => $ids,
220                'next'  => $next,
221            ]
222        );
223    }
224
225    /**
226     * Restrict a list of post types to those that are publicly viewable.
227     *
228     * Used to keep the public articles endpoint from exposing post types that
229     * WordPress would not surface on the front end. Non-viewable types are dropped;
230     * the returned list may be empty when none qualify (the caller then returns no results).
231     *
232     * @param array|string $post_types Requested post type(s).
233     * @return array Viewable post types (may be empty).
234     */
235    private static function filter_viewable_post_types( $post_types ) {
236        return array_values( array_filter( (array) $post_types, 'is_post_type_viewable' ) );
237    }
238
239    /**
240     * Sets up and returns attribute schema.
241     *
242     * @return array
243     */
244    public function get_attribute_schema() {
245        if ( empty( $this->attribute_schema ) ) {
246            $block_json = json_decode(
247                file_get_contents( __DIR__ . '/block.json' ), // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
248                true
249            );
250
251            $this->attribute_schema = array_merge(
252                $block_json['attributes'],
253                [
254                    'exclude_ids' => [
255                        'type'    => 'array',
256                        'default' => [],
257                        'items'   => [
258                            'type' => 'integer',
259                        ],
260                    ],
261                ]
262            );
263        }
264        return $this->attribute_schema;
265    }
266}