Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.92% covered (warning)
76.92%
60 / 78
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCOM_REST_API_V2_Endpoint_Email_Preview
80.00% covered (warning)
80.00%
60 / 75
40.00% covered (danger)
40.00%
2 / 5
15.57
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 register_routes
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
1
 permissions_check
95.65% covered (success)
95.65%
22 / 23
0.00% covered (danger)
0.00%
0 / 1
7
 is_authorized_blog_token_request
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 email_preview
20.00% covered (danger)
20.00%
2 / 10
0.00% covered (danger)
0.00%
0 / 1
4.05
1<?php
2/**
3 * Email Preview endpoint for the WordPress.com REST API.
4 *
5 * @package automattic/jetpack
6 */
7
8use Automattic\Jetpack\Connection\Manager;
9use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
10use Automattic\Jetpack\Status\Host;
11
12if ( ! defined( 'ABSPATH' ) ) {
13    exit( 0 );
14}
15
16/**
17 * Class WPCOM_REST_API_V2_Endpoint_Email_Preview
18 *
19 * Returns an email preview given a post id.
20 */
21class WPCOM_REST_API_V2_Endpoint_Email_Preview extends WP_REST_Controller {
22
23    use WPCOM_REST_API_Proxy_Request;
24
25    /**
26     * Constructor.
27     */
28    public function __construct() {
29        $this->base_api_path                   = 'wpcom';
30        $this->version                         = 'v2';
31        $this->namespace                       = $this->base_api_path . '/' . $this->version;
32        $this->rest_base                       = '/email-preview';
33        $this->wpcom_is_wpcom_only_endpoint    = true;
34        $this->wpcom_is_site_specific_endpoint = true;
35
36        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
37    }
38
39    /**
40     * Registers the routes for email preview.
41     *
42     * @see register_rest_route()
43     */
44    public function register_routes() {
45        $options = array(
46            'show_in_index'       => true,
47            'methods'             => 'GET',
48            'callback'            => array( $this, 'email_preview' ),
49            'permission_callback' => array( $this, 'permissions_check' ),
50            'args'                => array(
51                'post_id' => array(
52                    'description' => __( 'Unique identifier for the post.', 'jetpack' ),
53                    'type'        => 'integer',
54                ),
55                'access'  => array(
56                    'description'       => __( 'Access level.', 'jetpack' ),
57                    'enum'              => array( 'everybody', 'subscribers', 'paid_subscribers' ),
58                    'default'           => 'everybody',
59                    'validate_callback' => function ( $param ) {
60                        return in_array(
61                            $param,
62                            array( 'everybody', 'subscribers', 'paid_subscribers' ),
63                            true
64                        );
65                    },
66                ),
67            ),
68        );
69
70        register_rest_route(
71            $this->namespace,
72            $this->rest_base,
73            $options
74        );
75    }
76
77    /**
78     * Checks whether the request may render an email preview.
79     *
80     * The preview only renders the site's own post, so it needs the site to be connected
81     * but not the requesting user. The `edit_post` capability check is the authorization.
82     *
83     * @param WP_REST_Request $request Full data about the request.
84     *
85     * @return true|WP_Error True if the request may render the preview, WP_Error object otherwise.
86     */
87    public function permissions_check( $request ) {
88        $is_wpcom_simple = ( new Host() )->is_wpcom_simple();
89
90        // On a self-hosted site, the site must be connected before we can proxy the preview to WordPress.com.
91        // This uses its own error code (not the user-connection one) so the client can tell the two apart.
92        if ( ! $is_wpcom_simple && ! ( new Manager() )->is_connected() ) {
93            return new WP_Error(
94                'rest_cannot_view_email_preview',
95                __( 'Please connect your site to WordPress.com to preview emails.', 'jetpack' ),
96                array( 'status' => rest_authorization_required_code() )
97            );
98        }
99
100        $post = get_post( $request->get_param( 'post_id' ) );
101
102        if ( ! $post ) {
103            return new \WP_Error(
104                'post_not_found',
105                __( 'Post not found.', 'jetpack' ),
106                array( 'status' => 404 )
107            );
108        }
109
110        // Authorize any user who can edit the post: the local editor on self-hosted, a user-token request on WordPress.com.
111        if ( current_user_can( 'edit_post', $post->ID ) ) {
112            return true;
113        }
114
115        // On WordPress.com, a blog-token proxy authenticates as user 0 and fails the edit_post check
116        // above, so authorize it by site ownership instead. The self-hosted endpoint already verified a
117        // local editor could edit the post, and the blog token never reaches the browser.
118        if ( $is_wpcom_simple && $this->is_authorized_blog_token_request() ) {
119            return true;
120        }
121
122        return new WP_Error(
123            'rest_forbidden_context',
124            __( 'Sorry, you are not allowed to preview emails on this site.', 'jetpack' ),
125            array( 'status' => rest_authorization_required_code() )
126        );
127    }
128
129    /**
130     * Whether the request is a valid blog-token request authorized for the current site.
131     *
132     * Runs on WordPress.com, where a proxied blog token authenticates as user 0.
133     *
134     * @return bool True if the request is authorized for the current Jetpack site.
135     */
136    private function is_authorized_blog_token_request() {
137        if ( ! is_jetpack_site( get_current_blog_id() ) ) {
138            return false;
139        }
140
141        if ( ! class_exists( 'WPCOM_REST_API_V2_Endpoint_Jetpack_Auth' ) ) {
142            require_once dirname( __DIR__ ) . '/rest-api-plugins/endpoints/jetpack-auth.php';
143        }
144
145        $jp_auth_endpoint = new WPCOM_REST_API_V2_Endpoint_Jetpack_Auth();
146
147        return true === $jp_auth_endpoint->is_jetpack_authorized_for_site();
148    }
149
150    /**
151     * Returns an email preview of a post, or proxies the request to WordPress.com on non-wpcom sites.
152     *
153     * @param WP_REST_Request $request Full data about the request.
154     *
155     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
156     */
157    public function email_preview( $request ) {
158        // On a non-wpcom site, proxy to WordPress.com with the user's token, falling back to the site's blog token.
159        if ( ! ( new Host() )->is_wpcom_simple() ) {
160            return $this->proxy_request_to_wpcom( $request, '', 'user', true );
161        }
162
163        $post_id = $request['post_id'];
164        $access  = $request['access'];
165        $post    = get_post( $post_id );
166        return rest_ensure_response(
167            array(
168                /**
169                * Filters the generated email preview HTML.
170                *
171                * @since 13.8
172                *
173                * @param string $html   The generated HTML for the email preview.
174                * @param WP_Post $post  The post object.
175                * @param string $access The access level.
176                */
177                'html' => apply_filters( 'jetpack_generate_email_preview_html', '', $post, $access ),
178            )
179        );
180    }
181}
182
183wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Email_Preview' );