Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
89.36% covered (warning)
89.36%
42 / 47
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
REST_Controller
89.36% covered (warning)
89.36%
42 / 47
50.00% covered (danger)
50.00%
2 / 4
9.10
0.00% covered (danger)
0.00%
0 / 1
 register_routes
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
1
 enable_export
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 rotate_secret
63.64% covered (warning)
63.64%
7 / 11
0.00% covered (danger)
0.00%
0 / 1
2.19
 permission_check
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
1<?php
2/**
3 * REST controller for Jetpack Reprint export provisioning endpoints.
4 *
5 * Requires a verified Jetpack user token for a site administrator.
6 *
7 * @package automattic/jetpack
8 */
9
10namespace Automattic\Jetpack\Reprint_Export;
11
12use Automattic\Jetpack\Connection\Rest_Authentication;
13use WP_REST_Controller;
14use WP_REST_Response;
15use WP_REST_Server;
16
17/**
18 * Reprint exporter REST controller.
19 */
20class REST_Controller extends WP_REST_Controller {
21
22    /**
23     * The API namespace.
24     *
25     * @var string
26     */
27    protected $namespace = 'jetpack/v4';
28
29    /**
30     * The REST base path.
31     *
32     * @var string
33     */
34    protected $rest_base = 'reprint';
35
36    /**
37     * Registers the reprint export routes.
38     */
39    public function register_routes() {
40        register_rest_route(
41            $this->namespace,
42            '/' . $this->rest_base . '/rotate-export-secret',
43            array(
44                array(
45                    'methods'             => WP_REST_Server::CREATABLE,
46                    'callback'            => array( $this, 'rotate_secret' ),
47                    'permission_callback' => array( $this, 'permission_check' ),
48                ),
49            )
50        );
51
52        register_rest_route(
53            $this->namespace,
54            '/' . $this->rest_base . '/enable-export',
55            array(
56                array(
57                    'methods'             => WP_REST_Server::CREATABLE,
58                    'callback'            => array( $this, 'enable_export' ),
59                    'permission_callback' => array( $this, 'permission_check' ),
60                ),
61            )
62        );
63    }
64
65    /**
66     * Opens the export window without rotating the secret, so a client that
67     * already has one can reopen a window that has closed.
68     *
69     * Registered only where the feature is available, so clients also use a 404
70     * from here to tell whether the site supports export at all.
71     *
72     * @return WP_REST_Response The unix timestamp the window was opened at.
73     */
74    public function enable_export() {
75        $enabled_at = Reprint_Exporter::open_export_window();
76
77        Reprint_Exporter::record_event(
78            'window_opened',
79            array( 'user_id' => get_current_user_id() )
80        );
81
82        return new WP_REST_Response( array( 'enabled_at' => $enabled_at ), 200 );
83    }
84
85    /**
86     * Rotates the shared secret and returns it.
87     *
88     * Uses random_bytes() rather than wp_generate_password(). That helper is for
89     * passwords a person reads and types, and sites can filter it through
90     * `random_password` to enforce their own policy — an extension point we do
91     * not want on a credential. random_bytes() also throws rather than quietly
92     * falling back to a weaker source, which wp_rand() will do.
93     *
94     * @return WP_REST_Response The new secret on success, or a 500 error.
95     */
96    public function rotate_secret() {
97        $secret = bin2hex( random_bytes( 32 ) );
98
99        if ( ! Reprint_Exporter::store_secret( $secret ) ) {
100            return new WP_REST_Response(
101                array( 'error' => 'Failed to persist the new secret.' ),
102                500
103            );
104        }
105
106        Reprint_Exporter::record_event(
107            'secret_rotated',
108            array( 'user_id' => get_current_user_id() )
109        );
110
111        return new WP_REST_Response( array( 'secret' => $secret ), 200 );
112    }
113
114    /**
115     * Permission callback: a Jetpack-signed request from a site administrator.
116     *
117     * Deliberately a role check, not a capability one. This hands out a secret
118     * that streams the whole database and file tree, and no capability says
119     * that — `manage_options` is the closest, but plugins grant it to shop
120     * managers and the like.
121     *
122     * @return bool
123     */
124    public function permission_check() {
125        if ( ! Rest_Authentication::is_signed_with_user_token() ) {
126            return false;
127        }
128
129        $user = wp_get_current_user();
130        if ( ! $user || ! $user->exists() ) {
131            return false;
132        }
133
134        // Network administrator only: the export takes every table and everything
135        // under ABSPATH, so a subsite administrator would leave with every other
136        // site's users, content and uploads.
137        if ( is_multisite() ) {
138            return is_super_admin( $user->ID );
139        }
140
141        return in_array( 'administrator', $user->roles, true );
142    }
143}