Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
159 / 159
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
Services_Controller
100.00% covered (success)
100.00%
159 / 159
100.00% covered (success)
100.00%
10 / 10
24
100.00% covered (success)
100.00%
1 / 1
 register_routes
100.00% covered (success)
100.00%
77 / 77
100.00% covered (success)
100.00%
1 / 1
1
 get_items
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 update_enabled
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 create_item
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
3
 update_item
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 delete_item
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 services
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 unavailable
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 custom_service
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 prepare_service
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * The sharing services routes behind Settings > Sharing.
4 *
5 * @package automattic/jetpack-sharing-likes
6 */
7
8declare( strict_types = 1 );
9
10namespace Automattic\Jetpack\Sharing_Likes\REST;
11
12use Automattic\Jetpack\Sharing_Likes\Settings\Section_State;
13use Automattic\Jetpack\Sharing_Likes\Settings\Sharing_Options;
14use Automattic\Jetpack\Sharing_Likes\Settings\Sharing_Section;
15use WP_Error;
16use WP_REST_Request;
17use WP_REST_Response;
18use WP_REST_Server;
19
20/**
21 * Which services show, in which order, and the site's custom services.
22 *
23 * Available only where the screen shows the services list, which is also where
24 * sharedaddy is loaded to do the saving.
25 */
26final class Services_Controller extends Controller {
27
28    /**
29     * Register the routes.
30     */
31    public function register_routes() {
32        $base    = '/' . Endpoints::BASE . '/services';
33        $details = array(
34            'name' => array(
35                'description' => __( 'Service name.', 'jetpack-sharing-likes' ),
36                'type'        => 'string',
37            ),
38            'url'  => array(
39                'description' => __( 'Sharing URL, with placeholders for the post being shared.', 'jetpack-sharing-likes' ),
40                'type'        => 'string',
41            ),
42            'icon' => array(
43                'description' => __( 'URL of a 16×16 icon.', 'jetpack-sharing-likes' ),
44                'type'        => 'string',
45            ),
46        );
47
48        register_rest_route(
49            $this->namespace,
50            $base,
51            array(
52                array(
53                    'methods'             => WP_REST_Server::READABLE,
54                    'callback'            => array( $this, 'get_items' ),
55                    'permission_callback' => array( $this, 'permission_check' ),
56                ),
57                array(
58                    'methods'             => WP_REST_Server::EDITABLE,
59                    'callback'            => array( $this, 'update_enabled' ),
60                    'permission_callback' => array( $this, 'permission_check' ),
61                    'args'                => array(
62                        'visible' => array(
63                            'description' => __( 'Services shown as buttons, in order.', 'jetpack-sharing-likes' ),
64                            'type'        => 'array',
65                            'items'       => array( 'type' => 'string' ),
66                            'required'    => true,
67                        ),
68                        'hidden'  => array(
69                            'description' => __( 'Services behind the "More" button, in order.', 'jetpack-sharing-likes' ),
70                            'type'        => 'array',
71                            'items'       => array( 'type' => 'string' ),
72                            'required'    => true,
73                        ),
74                    ),
75                ),
76            )
77        );
78
79        register_rest_route(
80            $this->namespace,
81            $base . '/custom',
82            array(
83                'methods'             => WP_REST_Server::CREATABLE,
84                'callback'            => array( $this, 'create_item' ),
85                'permission_callback' => array( $this, 'permission_check' ),
86                'args'                => array_map(
87                    function ( $arg ) {
88                        return $arg + array( 'required' => true );
89                    },
90                    $details
91                ),
92            )
93        );
94
95        register_rest_route(
96            $this->namespace,
97            $base . '/custom/(?P<id>custom-\d+)',
98            array(
99                array(
100                    'methods'             => WP_REST_Server::EDITABLE,
101                    'callback'            => array( $this, 'update_item' ),
102                    'permission_callback' => array( $this, 'permission_check' ),
103                    'args'                => $details,
104                ),
105                array(
106                    'methods'             => WP_REST_Server::DELETABLE,
107                    'callback'            => array( $this, 'delete_item' ),
108                    'permission_callback' => array( $this, 'permission_check' ),
109                ),
110            )
111        );
112    }
113
114    /**
115     * The enabled services, and every service the site can enable.
116     *
117     * @param WP_REST_Request $request Request.
118     * @return WP_REST_Response|WP_Error
119     */
120    public function get_items( $request ) {
121        unset( $request );
122
123        $unavailable = self::unavailable();
124        if ( $unavailable ) {
125            return $unavailable;
126        }
127
128        return rest_ensure_response( self::services() );
129    }
130
131    /**
132     * Save which services show, and in which order.
133     *
134     * @param WP_REST_Request $request Request.
135     * @return WP_REST_Response|WP_Error
136     */
137    public function update_enabled( $request ) {
138        $unavailable = self::unavailable();
139        if ( $unavailable ) {
140            return $unavailable;
141        }
142
143        ( new \Sharing_Service() )->set_blog_services( $request->get_param( 'visible' ), $request->get_param( 'hidden' ) );
144
145        // Not `get_items()`: an empty save can hand the section to the block, closing these routes.
146        return rest_ensure_response( self::services() );
147    }
148
149    /**
150     * Create a custom service. It is not enabled until the enabled services are saved with it.
151     *
152     * @param WP_REST_Request $request Request.
153     * @return WP_REST_Response|WP_Error
154     */
155    public function create_item( $request ) {
156        $unavailable = self::unavailable();
157        if ( $unavailable ) {
158            return $unavailable;
159        }
160
161        $service = ( new \Sharing_Service() )->new_service(
162            $request->get_param( 'name' ),
163            $request->get_param( 'url' ),
164            $request->get_param( 'icon' )
165        );
166
167        if ( ! $service ) {
168            return new WP_Error(
169                'rest_sharing_likes_invalid_service',
170                __( 'A custom service needs a name, a sharing URL and an icon URL.', 'jetpack-sharing-likes' ),
171                array( 'status' => 400 )
172            );
173        }
174
175        return new WP_REST_Response( self::prepare_service( $service ), 201 );
176    }
177
178    /**
179     * Change a custom service, keeping whatever the request leaves out.
180     *
181     * @param WP_REST_Request $request Request.
182     * @return WP_REST_Response|WP_Error
183     */
184    public function update_item( $request ) {
185        $service = self::custom_service( $request->get_url_params()['id'] );
186        if ( $service instanceof WP_Error ) {
187            return $service;
188        }
189
190        $options = $service->get_options();
191
192        foreach ( array( 'name', 'url', 'icon' ) as $key ) {
193            if ( $request->has_param( $key ) ) {
194                $options[ $key ] = $request->get_param( $key );
195            }
196        }
197
198        // `update_options()` unslashes the name, as it was written for `$_POST`.
199        $options['name'] = wp_slash( $options['name'] );
200
201        $service->update_options( $options );
202        ( new \Sharing_Service() )->set_service( $service->get_id(), $service );
203
204        return rest_ensure_response( self::prepare_service( $service ) );
205    }
206
207    /**
208     * Delete a custom service.
209     *
210     * @param WP_REST_Request $request Request.
211     * @return WP_REST_Response|WP_Error
212     */
213    public function delete_item( $request ) {
214        $service = self::custom_service( $request->get_url_params()['id'] );
215        if ( $service instanceof WP_Error ) {
216            return $service;
217        }
218
219        ( new \Sharing_Service() )->delete_service( $service->get_id() );
220
221        return rest_ensure_response(
222            array(
223                'deleted' => true,
224                'id'      => $service->get_id(),
225            )
226        );
227    }
228
229    /**
230     * The enabled services, and every service the site can enable.
231     *
232     * @return array<string, array>
233     */
234    private static function services(): array {
235        $sharer  = new \Sharing_Service();
236        $enabled = $sharer->get_blog_services();
237
238        return array(
239            'visible'  => array_keys( $enabled['visible'] ),
240            'hidden'   => array_keys( $enabled['hidden'] ),
241            'services' => array_values( array_map( array( __CLASS__, 'prepare_service' ), $sharer->get_all_services_blog() ) ),
242        );
243    }
244
245    /**
246     * An error unless the screen shows the services list.
247     */
248    private static function unavailable(): ?WP_Error {
249        if ( Sharing_Options::is_available() && Section_State::configures( Sharing_Section::state() ) ) {
250            return null;
251        }
252
253        return new WP_Error(
254            'rest_sharing_likes_services_unavailable',
255            __( 'Sharing buttons are not available on this site right now.', 'jetpack-sharing-likes' ),
256            array( 'status' => 409 )
257        );
258    }
259
260    /**
261     * A custom service by ID, or the error to answer with.
262     *
263     * @param string $id Service ID.
264     * @return \Sharing_Advanced_Source|WP_Error
265     */
266    private static function custom_service( string $id ) {
267        $unavailable = self::unavailable();
268        if ( $unavailable ) {
269            return $unavailable;
270        }
271
272        $services = ( new \Sharing_Service() )->get_all_services_blog();
273
274        if ( isset( $services[ $id ] ) && $services[ $id ] instanceof \Sharing_Advanced_Source ) {
275            return $services[ $id ];
276        }
277
278        return new WP_Error(
279            'rest_sharing_likes_service_not_found',
280            __( 'No custom service with that ID.', 'jetpack-sharing-likes' ),
281            array( 'status' => 404 )
282        );
283    }
284
285    /**
286     * A service as the screen lists it.
287     *
288     * @param \Sharing_Source $service Service.
289     * @return array<string, mixed>
290     */
291    private static function prepare_service( $service ): array {
292        $item = array(
293            'id'         => $service->get_id(),
294            'name'       => wp_specialchars_decode( (string) $service->get_name(), ENT_QUOTES ),
295            'custom'     => $service instanceof \Sharing_Advanced_Source,
296            'deprecated' => $service->is_deprecated(),
297        );
298
299        if ( $service instanceof \Sharing_Advanced_Source ) {
300            $options      = $service->get_options();
301            $item['url']  = (string) ( $options['url'] ?? '' );
302            $item['icon'] = (string) ( $options['icon'] ?? '' );
303        }
304
305        return $item;
306    }
307}