Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
12.50% covered (danger)
12.50%
6 / 48
16.67% covered (danger)
16.67%
1 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Podcast_Distribution_Endpoint
12.50% covered (danger)
12.50%
6 / 48
16.67% covered (danger)
16.67%
1 / 6
187.50
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 register
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 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 permission_check
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 submit_pocket_casts
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 save_show_state
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
8
1<?php
2/**
3 * Local Jetpack-side REST proxy for podcast distribution submissions.
4 *
5 * @package automattic/jetpack-podcast
6 */
7
8namespace Automattic\Jetpack\Podcast;
9
10use Automattic\Jetpack\Connection\Client;
11use Jetpack_Options;
12use WP_Error;
13use WP_REST_Controller;
14use WP_REST_Response;
15use WP_REST_Server;
16
17/**
18 * Forwards podcast distribution `wp.apiFetch` calls from the dashboard SPA to
19 * the wpcom-side podcast-distribution endpoints. Same rationale as
20 * Podcast_Stats_Endpoint: wpcom-proxy-request can't authenticate from Atomic
21 * origins, so we proxy server-to-server with the user's token.
22 */
23class Podcast_Distribution_Endpoint extends WP_REST_Controller {
24
25    use Relay_Response;
26
27    /**
28     * Wire up routes.
29     */
30    public static function init() {
31        add_action( 'rest_api_init', array( self::class, 'register' ) );
32    }
33
34    /**
35     * Registers the REST routes on the `rest_api_init` hook.
36     *
37     * Instantiated here, rather than eagerly, so the endpoint class only loads
38     * on requests that reach `rest_api_init`. Static so the callback can be
39     * unregistered.
40     */
41    public static function register() {
42        ( new self() )->register_routes();
43    }
44
45    /**
46     * Register the Pocket Casts submit proxy route.
47     */
48    public function register_routes() {
49        $this->namespace = 'wpcom/v2';
50        $this->rest_base = 'podcast-distribution';
51
52        register_rest_route(
53            $this->namespace,
54            $this->rest_base . '/pocket-casts/submit',
55            array(
56                'methods'             => WP_REST_Server::CREATABLE,
57                'callback'            => array( $this, 'submit_pocket_casts' ),
58                'permission_callback' => array( $this, 'permission_check' ),
59            )
60        );
61    }
62
63    /**
64     * Permission callback. Submitting a feed mutates the site's distribution
65     * state, so require the same edit permission the dashboard SPA itself uses.
66     *
67     * @return true|WP_Error
68     */
69    public function permission_check() {
70        if ( ! current_user_can( 'edit_posts' ) ) {
71            return new WP_Error(
72                'rest_forbidden',
73                __( 'Sorry, you are not allowed to submit podcasts for this site.', 'jetpack-podcast' ),
74                array( 'status' => rest_authorization_required_code() )
75            );
76        }
77        return true;
78    }
79
80    /**
81     * POST /wpcom/v2/podcast-distribution/pocket-casts/submit — forward to wpcom.
82     *
83     * @return WP_REST_Response|WP_Error
84     */
85    public function submit_pocket_casts() {
86        $blog_id = (int) Jetpack_Options::get_option( 'id' );
87        if ( ! $blog_id ) {
88            return new WP_Error(
89                'site-not-connected',
90                __( 'Site is not connected to WordPress.com.', 'jetpack-podcast' ),
91                array( 'status' => 400 )
92            );
93        }
94
95        $response = Client::wpcom_json_api_request_as_user(
96            sprintf( '/sites/%d/podcast-distribution/pocket-casts/submit', $blog_id ),
97            '2',
98            array(
99                // Pocket Casts relay can be slow; keep a generous timeout.
100                'method'  => 'POST',
101                'headers' => array( 'content-type' => 'application/json' ),
102                'timeout' => 30,
103            ),
104            null,
105            'wpcom'
106        );
107
108        $relayed = $this->relay_response( $response );
109
110        if ( $relayed instanceof WP_REST_Response ) {
111            $this->save_show_state( $relayed->get_data() );
112        }
113
114        return $relayed;
115    }
116
117    /**
118     * Mirror the Pocket Casts verdict onto this site's local podcast options so
119     * the dashboard reflects it (wpcom only persisted it to its own copy).
120     *
121     * @param mixed $data Decoded relay body.
122     */
123    protected function save_show_state( $data ): void {
124        $state = is_array( $data ) && isset( $data['state'] ) ? $data['state'] : '';
125
126        if ( ! in_array( $state, array( 'pending', 'active', 'rejected' ), true ) ) {
127            return;
128        }
129
130        update_option( 'podcasting_show_states', array( 'pocketcasts' => 'rejected' === $state ? '' : $state ) );
131
132        if ( 'active' === $state && ! empty( $data['share_link'] ) && is_string( $data['share_link'] ) ) {
133            update_option( 'podcasting_show_urls', array( 'pocketcasts' => $data['share_link'] ) );
134        }
135    }
136}