| @@ -1,0 +1,143 @@ | ||
| 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 | + | |
| 10 | +namespace Automattic\Jetpack\Reprint_Export; | |
| 11 | + | |
| 12 | +use Automattic\Jetpack\Connection\Rest_Authentication; | |
| 13 | +use WP_REST_Controller; | |
| 14 | +use WP_REST_Response; | |
| 15 | +use WP_REST_Server; | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * Reprint exporter REST controller. | |
| 19 | + */ | |
| 20 | +class 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 | +} | |