├── .gitignore ├── README.md ├── admin.php ├── classes ├── admin.php ├── disable-rest-api.php ├── helpers.php ├── index.php └── requirements-check.php ├── css └── admin.css ├── disable-json-api.php ├── docs ├── _config.yml └── index.md ├── index.php ├── js ├── admin-footer.js └── admin-header.js ├── languages └── index.php ├── readme.txt └── uninstall.php /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | \.idea/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Disable REST API 2 | 3 | [](https://codeclimate.com/github/dmchale/disable-json-api) [](https://www.codacy.com/gh/dmchale/disable-json-api/dashboard?utm_source=github.com&utm_medium=referral&utm_content=dmchale/disable-json-api&utm_campaign=Badge_Grade) 4 | 5 | ** This is the public repository for the latest DEVELOPMENT copy of the plugin. There is absolutely no guarantee, 6 | express or implied, that the code you find here is a stable build. For official releases, please see the 7 | WordPress repository at https://wordpress.org/plugins/disable-json-api/ ** 8 | 9 | Disable the use of the REST API on your website to unauthenticated users, with the freedom to enable individual 10 | routes as desired. Manage route access for logged-in users based on their User Role. 11 | 12 | ## Installation 13 | 1. Install to WordPress plugins as normal and activate. 14 | ## Usage 15 | 1. Basic usage of the plugin requires no configuration. 16 | 2. Optionally, you may use the Settings page to whitelist individual routes inside the REST API based on User Role (Unauthenticated Users as well as any logged-in user) 17 | ## History 18 | 1. Initial versions of this plugin simply used the existing filters of the REST API to disable it entirely. 19 | 2. As of WordPress 4.7 and version 1.3 of this plugin, the plugin would forcibly throw an authentication error for unauthenticated users. 20 | 3. In version 1.4 we introduced the Settings screen and allow site admins to whitelist routes they wish to allow for unauthenticated users. 21 | 4. In version 1.5 we added minimum requirements checks for WordPress and PHP. Fixed minor bug to prevent unintended empty routes. Minor text & text-domain updates. 22 | 5. In version 1.6 we added support for per-role rules and did a number of other housekeeping updates in the code. 23 | 6. In version 1.7 we changed how we cache-bust static file enqueues, and repaired a logic bug in the role-based default_allow checks. 24 | 7. In version 1.8 we provided a new filter so devs can customize the error message sent back if you fail the authentication check; updated minimum requirements to PHP 5.6 (up from 5.3) and WordPress 4.9 (up from WP 4.4); patched a Fatal Error when activating plugin on installations running LearnDash. 25 | ## Credits 26 | Authored by Dave McHale. Contributed to by Tang Rufus. 27 | ## License 28 | As with all WordPress projects, this plugin is released under the GPL 29 | -------------------------------------------------------------------------------- /admin.php: -------------------------------------------------------------------------------- 1 |
7 | 8 |
9 | 10 |81 | '; 84 | echo esc_html__( 'If you choose to manage access for a user role, you will have to come back and add permissions for any new routes later.', 'disable-json-api' ); 85 | ?> 86 |
87 | 88 | 89 | 90 | base_file_path = plugin_basename( $path ); 30 | 31 | // Do logic for upgrading to 1.6 from versions less than 1.6 32 | add_action( 'wp_loaded', array( &$this, 'option_check' ) ); 33 | 34 | // Set up admin page for plugin settings 35 | add_action( 'admin_menu', array( &$this, 'define_admin_link' ) ); 36 | 37 | // This actually does everything in this plugin 38 | add_filter( 'rest_authentication_errors', array( &$this, 'you_shall_not_pass' ), 20 ); 39 | 40 | } 41 | 42 | 43 | /** 44 | * Checks for a current route being requested, and processes the allowlist 45 | * 46 | * @param $access 47 | * 48 | * @return WP_Error|null|boolean 49 | */ 50 | public function you_shall_not_pass( $access ) { 51 | 52 | // Return current value of $access and skip all plugin functionality 53 | if ( $this->allow_rest_api() ) { 54 | return $access; 55 | } 56 | 57 | $current_route = $this->get_current_route(); 58 | 59 | if ( ! $this->is_route_allowed( $current_route ) ) { 60 | return $this->get_wp_error( $access ); 61 | } 62 | 63 | // If we got all the way here, return the unmodified $access response 64 | return $access; 65 | 66 | } 67 | 68 | 69 | /** 70 | * Current REST route getter. 71 | * 72 | * @return string 73 | */ 74 | private function get_current_route() { 75 | $rest_route = isset( $GLOBALS['wp']->query_vars['rest_route'] ) ? 76 | $GLOBALS['wp']->query_vars['rest_route'] : 77 | ''; 78 | 79 | return ( empty( $rest_route ) || '/' == $rest_route ) ? 80 | $rest_route : 81 | untrailingslashit( $rest_route ); 82 | } 83 | 84 | 85 | /** 86 | * Checks a route for whether it belongs to the list of allowed routes 87 | * 88 | * @param $currentRoute 89 | * 90 | * @return boolean 91 | */ 92 | private function is_route_allowed( $currentRoute ) { 93 | 94 | $current_options = get_option( 'disable_rest_api_options', array() ); 95 | $current_user_roles = $this->get_current_user_roles(); 96 | 97 | // Loop through user roles belonging to the current user 98 | foreach ( $current_user_roles as $role ) { 99 | 100 | // If we have a definition for the current user's role 101 | if ( isset( $current_options['roles'][ $role ] ) ) { 102 | 103 | // If any role for this user is set to Allow Full REST API Access, return true automatically 104 | if ( true === $current_options['roles'][ $role ]['default_allow'] ) { 105 | return true; 106 | } 107 | 108 | // See if this route is specifically allowed 109 | $is_currentRoute_allowed = array_reduce( DRA_Helpers::get_allowed_routes( $role ), function ( $isMatched, $pattern ) use ( $currentRoute ) { 110 | return $isMatched || (bool) preg_match( '@^' . htmlspecialchars_decode( $pattern ) . '$@i', $currentRoute ); 111 | }, false ); 112 | if ( $is_currentRoute_allowed ) { 113 | return true; 114 | } 115 | 116 | // See if this route is specifically disallowed 117 | $is_currentRoute_disallowed = array_reduce( DRA_Helpers::get_allowed_routes( $role, false ), function ( $isMatched, $pattern ) use ( $currentRoute ) { 118 | return $isMatched || (bool) preg_match( '@^' . htmlspecialchars_decode( $pattern ) . '$@i', $currentRoute ); 119 | }, false ); 120 | if ( $is_currentRoute_disallowed ) { 121 | return false; 122 | } 123 | 124 | } 125 | 126 | } 127 | 128 | // If we got all the way here, we didn't find any rules that matched the route and none of the user roles had a "default unknowns to true" rule. 129 | // Most likely, we're here because the request is from a user role we don't have a definition for. 130 | // Return the plugin-global setting for what should be done in the case of something we don't know what to do with. 131 | // As of this writing in v1.6, this is "allow" by default since we want new User Roles to be ALLOWED access to everything until an admin chooses to take that right away. 132 | return $current_options['default_allow']; 133 | 134 | } 135 | 136 | 137 | /** 138 | * Add a menu 139 | * 140 | * @return void 141 | */ 142 | public function define_admin_link() { 143 | 144 | add_options_page( 145 | esc_html__( 'Disable REST API Settings', 'disable-json-api' ), 146 | esc_html__( 'Disable REST API', 'disable-json-api' ), 147 | self::CAPABILITY, 148 | self::MENU_SLUG, 149 | array( &$this, 'settings_page' ) 150 | ); 151 | add_filter( "plugin_action_links_$this->base_file_path", array( &$this, 'settings_link' ) ); 152 | add_action( 'admin_enqueue_scripts', array( &$this, 'admin_enqueues' ) ); 153 | 154 | } 155 | 156 | 157 | /** 158 | * Add Settings Link to plugins page 159 | * 160 | * @param $links 161 | * 162 | * @return array 163 | */ 164 | public function settings_link( $links ) { 165 | 166 | $settings_url = menu_page_url( self::MENU_SLUG, false ); 167 | $settings_link = "" . esc_html__( "Settings", "disable-json-api" ) . ""; 168 | array_unshift( $links, $settings_link ); 169 | 170 | return $links; 171 | } 172 | 173 | 174 | /** 175 | * Menu Callback 176 | * 177 | * @return void 178 | */ 179 | public function settings_page() { 180 | 181 | $this->maybe_process_settings_form(); 182 | 183 | // Render the settings template 184 | include( __DIR__ . "/../admin.php" ); 185 | 186 | } 187 | 188 | /** 189 | * Enqueues for adding CSS and JavaScript to the admin settings page 190 | */ 191 | public function admin_enqueues( $hook_suffix ) { 192 | if ( $hook_suffix == 'settings_page_' . self::MENU_SLUG ) { 193 | wp_enqueue_style( 'dra-admin-css', plugins_url( 'css/admin.css', $this->base_file_path ), array(), DISABLE_REST_API_PLUGIN_VER, 'all' ); 194 | wp_enqueue_script( 'dra-admin-header', plugins_url( 'js/admin-header.js', $this->base_file_path ), array( 'jquery' ), DISABLE_REST_API_PLUGIN_VER, false ); 195 | wp_enqueue_script( 'dra-admin-footer', plugins_url( 'js/admin-footer.js', $this->base_file_path ), array( 'jquery' ), DISABLE_REST_API_PLUGIN_VER, true ); 196 | } 197 | } 198 | 199 | 200 | /** 201 | * Process the admin page settings form submission 202 | * 203 | * @return void 204 | */ 205 | private function maybe_process_settings_form() { 206 | 207 | if ( ! ( isset( $_POST['_wpnonce'] ) && check_admin_referer( 'DRA_admin_nonce' ) ) ) { 208 | return; 209 | } 210 | 211 | if ( ! current_user_can( self::CAPABILITY ) ) { 212 | return; 213 | } 214 | 215 | // Confirm a valid role has been passed 216 | $role = ( isset( $_POST['role'] ) ) ? $_POST['role'] : 'dra-undefined'; 217 | if ( ! DRA_Helpers::is_valid_role( $role ) ) { 218 | add_settings_error( 'DRA-notices', esc_attr( 'settings_updated' ), esc_html__( 'Invalid user role detected when processing form. No updates have been made.', 'disable-json-api' ), 'error' ); 219 | 220 | return; 221 | } 222 | 223 | // Catch the `default_allow` value for this role 224 | $default_allow = ( isset( $_POST['default_allow'] ) && "1" == $_POST['default_allow'] ) ? true : false; 225 | 226 | // Catch the routes that should be allowed 227 | $rest_routes = ( isset( $_POST['rest_routes'] ) ) ? wp_unslash( $_POST['rest_routes'] ) : array(); 228 | 229 | // Retrieve all current rules for all roles 230 | $arr_option = get_option( 'disable_rest_api_options' ); 231 | 232 | // If resetting or allowlist is empty, clear the option and exit the function 233 | if ( empty( $rest_routes ) || isset( $_POST['reset'] ) ) { 234 | 235 | // Unauthorized users default to no routes allowed. All other user roles default to allowing all routes 236 | $rest_routes_for_setting = DRA_Helpers::build_routes_rule_for_all( $default_allow ); 237 | $msg = esc_html__( 'All allowlists have been reset for this user role.', 'disable-json-api' ); 238 | 239 | } else { 240 | 241 | // Get back the full list of true/false routes based on the posted routes allowed 242 | $rest_routes_for_setting = DRA_Helpers::build_routes_rule( $rest_routes ); 243 | $msg = esc_html__( 'Allowlist settings saved for this user role.', 'disable-json-api' ); 244 | 245 | } 246 | 247 | // Save only the rules for this role back to itself 248 | $arr_option['roles'][ $role ] = array( 249 | 'default_allow' => $default_allow, 250 | 'allow_list' => $rest_routes_for_setting, 251 | ); 252 | 253 | // Save allowlist to the Options table and return with message for user 254 | update_option( 'disable_rest_api_options', $arr_option ); 255 | add_settings_error( 'DRA-notices', esc_attr( 'settings_updated' ), $msg, 'updated' ); 256 | 257 | } 258 | 259 | 260 | /** 261 | * Allow carte blanche access for logged-in users (or allow override via filter) 262 | * 263 | * @return bool 264 | */ 265 | private function allow_rest_api() { 266 | return (bool) apply_filters( 'dra_allow_rest_api', false ); 267 | } 268 | 269 | 270 | /** 271 | * If $access is already a WP_Error object, add our error to the list 272 | * Otherwise return a new one 273 | * 274 | * @param $access 275 | * 276 | * @return WP_Error 277 | */ 278 | private function get_wp_error( $access ) { 279 | $dra_error_message = apply_filters( 'dra_error_message', 'DRA: Only authenticated users can access the REST API.', $access ); 280 | $error_message = esc_html__( $dra_error_message, 'disable-json-api' ); 281 | 282 | if ( is_wp_error( $access ) ) { 283 | $access->add( 'rest_cannot_access', $error_message, array( 'status' => rest_authorization_required_code() ) ); 284 | 285 | return $access; 286 | } 287 | 288 | return new WP_Error( 'rest_cannot_access', $error_message, array( 'status' => rest_authorization_required_code() ) ); 289 | } 290 | 291 | 292 | /** 293 | * Helper function to migrate from pre-version-1.6 to the new option 294 | */ 295 | public function option_check() { 296 | 297 | // If our new option already exists, we can bail 298 | if ( get_option( 'disable_rest_api_options' ) ) { 299 | return; 300 | } 301 | 302 | // Make sure we have a default option defined 303 | $this->create_settings_option(); 304 | 305 | } 306 | 307 | 308 | /** 309 | * Create settings option for the plugin 310 | */ 311 | private function create_settings_option() { 312 | 313 | // Define the basic structure of our new option 314 | $arr_option = array( 315 | 'version' => DISABLE_REST_API_PLUGIN_VER, // the current version of this plugin 316 | 'default_allow' => true, // if a role is not specifically defined in the settings, should the default be to ALLOW the route or not? 317 | 'roles' => array(), // array of the user roles in this install of wordpress 318 | ); 319 | 320 | // Default list of allowed routes. By default, nothing is allowed because we're checking for our pre-v1.6 option here for migration purposes 321 | $pre_1_6_allowed_routes = get_option( 'DRA_route_whitelist', array() ); 322 | 323 | // Decode the html encoding before passing to the function that builds the new routes. They'll get re-encoded later 324 | $pre_1_6_allowed_routes = array_map( 'html_entity_decode', $pre_1_6_allowed_routes ); 325 | 326 | // Build the rules for this role based on the merge with the previously allowed rules (if any) 327 | $new_unauthenticated_rules = DRA_Helpers::build_routes_rule( $pre_1_6_allowed_routes ); 328 | 329 | // Define the "unauthenticated" rules based on the old option value (or default value of "nothing") 330 | $arr_option['roles']['none'] = array( 331 | 'default_allow' => false, 332 | 'allow_list' => $new_unauthenticated_rules, 333 | ); 334 | 335 | // Save new option 336 | update_option( 'disable_rest_api_options', $arr_option ); 337 | 338 | // delete the old option if applicable 339 | if ( ! empty( $pre_1_6_allowed_routes ) ) { 340 | delete_option( 'DRA_route_whitelist' ); 341 | } 342 | 343 | } 344 | 345 | 346 | /** 347 | * Return array with list of roles the current user belongs to 348 | * 349 | * @return array 350 | */ 351 | private function get_current_user_roles() { 352 | if ( ! is_user_logged_in() ) { 353 | return array( 354 | 'name' => 'none', 355 | ); 356 | } 357 | 358 | $user = wp_get_current_user(); 359 | 360 | return ( array ) $user->roles; 361 | 362 | } 363 | 364 | } 365 | -------------------------------------------------------------------------------- /classes/helpers.php: -------------------------------------------------------------------------------- 1 | get_routes() ); 14 | } 15 | 16 | 17 | /** 18 | * Make sure this is called after wp-settings.php is loaded, or the `rest_get_server()` will throw 500's 19 | * 20 | * @return string[] 21 | */ 22 | static function get_all_rest_namespaces() { 23 | $wp_rest_server = rest_get_server(); 24 | 25 | return $wp_rest_server->get_namespaces(); 26 | } 27 | 28 | 29 | /** 30 | * Make sure this is called after wp-settings.php is loaded, or the `self::get_all_rest_routes()` will throw 500's 31 | * 32 | * @param $allowed_routes 33 | * 34 | * @return array 35 | */ 36 | static function build_routes_rule( $allowed_routes ) { 37 | 38 | // The full list of all routes in the system 39 | $all_routes = self::get_all_rest_routes(); 40 | 41 | // Initialize our new rules 42 | $new_rules = array(); 43 | 44 | // Loop through ALL routes, find out if any exist in the previously-existing rules. If so, they SHOULD be allowed. Default for everyone is false 45 | foreach ( $all_routes as $route ) { 46 | $new_value = false; 47 | if ( ! empty( $allowed_routes ) && in_array( $route, $allowed_routes ) ) { 48 | $new_value = true; 49 | } 50 | $new_rules[ esc_html( $route ) ] = $new_value; 51 | } 52 | 53 | // Return full list of all known routes, with true/false values for whether they are allowed 54 | return $new_rules; 55 | } 56 | 57 | 58 | /** 59 | * Make sure this is called after wp-settings.php is loaded, or the `self::get_all_rest_routes()` will throw 500's 60 | * 61 | * @param bool $default_value 62 | * 63 | * @return array 64 | */ 65 | static function build_routes_rule_for_all( $default_value = true ) { 66 | // The full list of all routes in the system 67 | $all_routes = self::get_all_rest_routes(); 68 | 69 | // Initialize our new rules 70 | $new_rules = array(); 71 | 72 | // Loop through ALL routes, set all to the desired value 73 | foreach ( $all_routes as $route ) { 74 | $new_rules[ esc_html( $route ) ] = $default_value; 75 | } 76 | 77 | // Return full list of all known routes with values defined 78 | return $new_rules; 79 | } 80 | 81 | 82 | /** 83 | * Confirms if the passed value is either 'none' or another role defined in the system 84 | * 85 | * @param $role 86 | * 87 | * @return bool 88 | */ 89 | static function is_valid_role( $role ) { 90 | 91 | // If we requested 'none', we know it's okay 92 | if ( 'none' == $role ) { 93 | return true; 94 | } 95 | 96 | // Get all roles from the system. Loop through and see if one of them is the one we're asking about 97 | $editable_roles = get_editable_roles(); 98 | foreach ( $editable_roles as $editable_role => $details ) { 99 | if ( $role == $editable_role ) { 100 | return true; 101 | } 102 | } 103 | 104 | // If we got here, we're trying to ask for an invalid user role 105 | return false; 106 | } 107 | 108 | 109 | /** 110 | * Check the WP Option for our stored values of which routes should be allowed based on the supplied role 111 | * 112 | * @param $role 113 | * @param bool $get_allowed 114 | * 115 | * @return array 116 | */ 117 | static function get_allowed_routes( $role, $get_allowed = true ) { 118 | $arr_option = get_option( 'disable_rest_api_options', array() ); 119 | 120 | // If we have an empty array, just return that 121 | if ( empty( $arr_option ) ) { 122 | return $arr_option; 123 | } 124 | 125 | $option_rules = array(); 126 | $allowed_rules = array(); 127 | 128 | if ( 'none' == $role && ! isset( $arr_option['roles']['none'] ) ) { 129 | 130 | // This helps us bridge the gap from plugin version <=1.5.1 to >=1.6. 131 | // We didn't use to store results based on role, but we want to return the values for "unauthenticated users" if we have recently upgraded 132 | $option_rules = ( array ) DRA_Helpers::build_routes_rule( $arr_option ); 133 | 134 | } elseif ( isset( $arr_option['roles'][ $role ]['allow_list'] ) ) { 135 | 136 | // If we have a definition for the currently requested role, return it 137 | $option_rules = ( array ) $arr_option['roles'][ $role ]['allow_list']; 138 | 139 | } else { 140 | 141 | // If we failed all the way down to here, return a default array since we're asking for a role we don't have a definition for yet 142 | $option_rules = ( array ) DRA_Helpers::build_routes_rule_for_all( true ); 143 | 144 | } 145 | 146 | // Loop through and only save the keys that have a value pairing of true 147 | foreach ( $option_rules as $key => $value ) { 148 | if ( $get_allowed === $value ) { 149 | $allowed_rules[] = $key; 150 | } 151 | } 152 | 153 | // Get rid of < and > before doing our comparisons 154 | $allowed_rules = array_map( 'htmlspecialchars_decode', $allowed_rules ); 155 | 156 | // Return our array of allowed rules 157 | return $allowed_rules; 158 | 159 | } 160 | 161 | 162 | /** 163 | * Return the setting for what the default route behavior is for a specified role 164 | * 165 | * @param $role 166 | * 167 | * @return bool 168 | */ 169 | static function get_default_allow_for_role( $role ) { 170 | $arr_option = get_option( 'disable_rest_api_options', array() ); 171 | 172 | // If we have an empty array, return false so we deny access 173 | if ( empty( $arr_option ) ) { 174 | return false; 175 | } 176 | 177 | // Unauthorized users default to DONT ALLOW, authorized users default to DO ALLOW 178 | $default_allow = ( 'none' == $role ) ? false : true; 179 | 180 | if ( isset( $arr_option['roles'][ $role ]['default_allow'] ) ) { 181 | $default_allow = $arr_option['roles'][ $role ]['default_allow']; 182 | } 183 | 184 | // Return our default rule 185 | return ( bool ) $default_allow; 186 | 187 | } 188 | 189 | 190 | /** 191 | * Returns the translated name of the role based on provided role slug 192 | * 193 | * @param $role 194 | * 195 | * @return string 196 | */ 197 | static function get_role_name( $role ) { 198 | 199 | if ( 'none' == $role ) { 200 | return __( 'Unauthenticated', 'disable-json-api' ); 201 | } 202 | 203 | $editable_roles = get_editable_roles(); 204 | if ( isset( $editable_roles[ $role ] ) ) { 205 | return translate_user_role( $editable_roles[ $role ]['name'] ); 206 | } 207 | 208 | return ''; 209 | 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /classes/index.php: -------------------------------------------------------------------------------- 1 | $setting = $args[ $setting ]; 19 | } 20 | } 21 | } 22 | 23 | public function passes() { 24 | $passes = $this->php_passes() && $this->wp_passes(); 25 | if ( ! $passes ) { 26 | add_action( 'admin_notices', array( $this, 'deactivate' ) ); 27 | } 28 | 29 | return $passes; 30 | } 31 | 32 | public function deactivate() { 33 | if ( isset( $this->file ) ) { 34 | deactivate_plugins( plugin_basename( $this->file ) ); 35 | } 36 | } 37 | 38 | private function php_passes() { 39 | if ( $this->__php_at_least( $this->php ) ) { 40 | return true; 41 | } else { 42 | add_action( 'admin_notices', array( $this, 'php_version_notice' ) ); 43 | 44 | return false; 45 | } 46 | } 47 | 48 | private static function __php_at_least( $min_version ) { 49 | return version_compare( phpversion(), $min_version, '>=' ); 50 | } 51 | 52 | public function php_version_notice() { 53 | echo 'The “" . esc_html( $this->title ) . "” plugin cannot run on PHP versions older than " . $this->php . '. Please contact your host and ask them to upgrade.
'; 55 | echo 'The “" . esc_html( $this->title ) . "” plugin cannot run on WordPress versions older than " . $this->wp . '. Please update WordPress.
'; 75 | echo '