Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.55% covered (success)
93.55%
29 / 31
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Blog_Posts
93.55% covered (success)
93.55%
29 / 31
33.33% covered (danger)
33.33%
1 / 3
9.02
0.00% covered (danger)
0.00%
0 / 1
 register_rest_endpoints
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 permissions_callback
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_blog_posts
95.24% covered (success)
95.24%
20 / 21
0.00% covered (danger)
0.00%
0 / 1
7
1<?php
2/**
3 * Recent Jetpack.com blog posts for the My Jetpack Overview.
4 *
5 * @package automattic/my-jetpack
6 */
7
8namespace Automattic\Jetpack\My_Jetpack;
9
10use WP_REST_Response;
11
12/**
13 * Registers the REST route that lists recent Jetpack.com blog posts.
14 */
15class REST_Blog_Posts {
16    /**
17     * The feed the Overview lists posts from.
18     *
19     * @var string
20     */
21    const FEED_URL = 'https://jetpack.com/feed/';
22
23    /**
24     * How many posts the Overview lists.
25     *
26     * @var int
27     */
28    const POST_COUNT = 3;
29
30    /**
31     * Site transient set while the feed is failing, so an outage costs one fetch an hour.
32     *
33     * @var string
34     */
35    const FAILED_TRANSIENT = 'jetpack_my_jetpack_blog_posts_failed';
36
37    /**
38     * Register the route.
39     *
40     * @return void
41     */
42    public static function register_rest_endpoints() {
43        register_rest_route(
44            'my-jetpack/v1',
45            'blog-posts',
46            array(
47                'methods'             => \WP_REST_Server::READABLE,
48                'callback'            => __CLASS__ . '::get_blog_posts',
49                'permission_callback' => __CLASS__ . '::permissions_callback',
50            )
51        );
52    }
53
54    /**
55     * Check user capability to access the endpoint.
56     *
57     * @return bool
58     */
59    public static function permissions_callback() {
60        return current_user_can( 'edit_posts' );
61    }
62
63    /**
64     * Recent posts from the feed, or none when it fails to load.
65     *
66     * @return WP_REST_Response
67     */
68    public static function get_blog_posts() {
69        // Site transients, like core's feed cache, so a multisite network retries once, not per site.
70        if ( get_site_transient( self::FAILED_TRANSIENT ) ) {
71            return rest_ensure_response( array() );
72        }
73
74        // fetch_feed() caches a parsed feed for 12 hours, but never a failure.
75        $feed = fetch_feed( self::FEED_URL );
76
77        if ( is_wp_error( $feed ) ) {
78            set_site_transient( self::FAILED_TRANSIENT, 1, HOUR_IN_SECONDS );
79            return rest_ensure_response( array() );
80        }
81
82        $posts = array();
83        foreach ( $feed->get_items( 0, self::POST_COUNT ) as $item ) {
84            $link = esc_url_raw( (string) $item->get_permalink() );
85            if ( '' === $link ) {
86                continue;
87            }
88
89            $enclosure = $item->get_enclosure();
90            $thumbnail = $enclosure ? esc_url_raw( (string) $enclosure->get_thumbnail() ) : '';
91
92            $posts[] = array(
93                // The feed parser returns entity-encoded text; the client renders plain text.
94                'title'     => html_entity_decode( (string) $item->get_title(), ENT_QUOTES, 'UTF-8' ),
95                'link'      => $link,
96                'date'      => $item->get_date( 'c' ),
97                'excerpt'   => wp_trim_words( html_entity_decode( (string) $item->get_description(), ENT_QUOTES, 'UTF-8' ), 20, '…' ),
98                // Jetpack.com crops on request: 2x the 56px thumbnail in overview-blog-posts/style.module.scss.
99                'thumbnail' => '' === $thumbnail ? null : add_query_arg( 'resize', '112,112', $thumbnail ),
100            );
101        }
102
103        return rest_ensure_response( $posts );
104    }
105}