├── .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 | [![Code Climate](https://codeclimate.com/github/dmchale/disable-json-api/badges/gpa.svg)](https://codeclimate.com/github/dmchale/disable-json-api) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/9d636a2e10534acc98531cde0625a7e7)](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 |
2 |

3 | 4 | 5 |

6 |

7 | 8 |

9 | 10 |
11 | 12 |
13 | : 20 |
21 | 22 |
23 | 24 |
25 | 26 | 27 | 28 |
29 | 30 |
31 | 32 |
33 | 34 |
35 | 36 |
37 |
38 | 39 |
40 | 41 | " 43 | onclick="return confirm('');"> 44 |
45 | 46 |
47 | 48 |
49 | -------------------------------------------------------------------------------- /classes/admin.php: -------------------------------------------------------------------------------- 1 | "; 26 | } 27 | 28 | $route_for_display = ( "/" == $route ) ? "/ " . esc_html__( "REST API ROOT", "disable-json-api" ) . "" : esc_html( $route ); 29 | echo "

"; 42 | } 43 | 44 | 45 | /** 46 | * During comparison, encode the route being requested in the same fashion that it's stored in the database option 47 | * Encoding during save happens in Disable_REST_API::maybe_process_settings_form() 48 | * 49 | * @param $route 50 | * @param $allowed_routes 51 | * 52 | * @return string 53 | */ 54 | static function get_route_checked_prop( $route, $allowed_routes ) { 55 | $is_route_checked = in_array( esc_html( $route ), array_map( 'esc_html', $allowed_routes ), true ); 56 | 57 | return checked( $is_route_checked, true, false ); 58 | } 59 | 60 | 61 | /** 62 | * Displays setting for default role on admin page 63 | * 64 | * @param $role 65 | */ 66 | static function display_role_default_allow( $role ) { 67 | $default_allow_true_checked = ''; 68 | $default_allow_false_checked = ''; 69 | 70 | $role_default_allow = DRA_Helpers::get_default_allow_for_role( $role ); 71 | if ( $role_default_allow ) { 72 | $default_allow_true_checked = ' checked="checked"'; 73 | } else { 74 | $default_allow_false_checked = ' checked="checked"'; 75 | } 76 | 77 | /* translators: name of user role */ 78 | echo sprintf( '

%s

', sprintf( esc_html__( 'Manage Rules for %s Users', 'disable-json-api' ), DRA_Helpers::get_role_name( $role ) ) ); 79 | ?> 80 |

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 '
'; 54 | 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 '
'; 56 | } 57 | 58 | private function wp_passes() { 59 | if ( $this->__wp_at_least( $this->wp ) ) { 60 | return true; 61 | } else { 62 | add_action( 'admin_notices', array( $this, 'wp_version_notice' ) ); 63 | 64 | return false; 65 | } 66 | } 67 | 68 | private static function __wp_at_least( $min_version ) { 69 | return version_compare( get_bloginfo( 'version' ), $min_version, '>=' ); 70 | } 71 | 72 | public function wp_version_notice() { 73 | echo '
'; 74 | echo "

The “" . esc_html( $this->title ) . "” plugin cannot run on WordPress versions older than " . $this->wp . '. Please update WordPress.

'; 75 | echo '
'; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /css/admin.css: -------------------------------------------------------------------------------- 1 | h2 { display: inline; } 2 | #route-container ul li { padding-left: 20px; } 3 | #route-container em { font-size: 0.8em; } 4 | .switch { position: relative; display: inline-block; width: 38px; height: 20px; margin-right: 0.4em; } 5 | .switch input { opacity: 0; width: 0; height: 0; } 6 | .slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; -webkit-transition: 0.4s; transition: 0.4s; border-radius: 18px; display:inline; } 7 | .slider::before { position: absolute; content: ""; height: 14px; width: 14px; left: 4px; bottom: 3px; background-color: #fff; -webkit-transition: 0.4s; transition: 0.4s; border-radius: 50%; } 8 | input:checked + .slider { background-color: #2196f3; } 9 | input:focus + .slider { box-shadow: 0 0 1px #2196f3; } 10 | input:checked + .slider::before { -webkit-transform: translateX(16px); -ms-transform: translateX(16px); transform: translateX(16px); } 11 | div#select-container, div#default-allow-container, div#route-container { padding: 1em 0 1em 0; } 12 | div#route-container { display: none; } 13 | input#dra-reset-button { display: none; } 14 | -------------------------------------------------------------------------------- /disable-json-api.php: -------------------------------------------------------------------------------- 1 | 'Disable REST API', 35 | 'php' => '5.6', 36 | 'wp' => '4.9', 37 | 'file' => __FILE__, 38 | ) ); 39 | 40 | // Only load plugin if we pass minimum requirements 41 | if ( $dra_requirements_check->passes() ) { 42 | 43 | // Remove REST API info from head and headers 44 | remove_action( 'xmlrpc_rsd_apis', 'rest_output_rsd' ); 45 | remove_action( 'wp_head', 'rest_output_link_wp_head', 10 ); 46 | remove_action( 'template_redirect', 'rest_output_link_header', 11 ); 47 | 48 | // Load in extra classes 49 | require_once( plugin_dir_path( __FILE__ ) . 'classes/helpers.php' ); 50 | 51 | // Only load admin classes if in admin area 52 | if ( is_admin() ) { 53 | require_once( plugin_dir_path( __FILE__ ) . 'classes/admin.php' ); 54 | } 55 | 56 | // Load the primary Disable_REST_API class 57 | require_once( plugin_dir_path( __FILE__ ) . 'classes/disable-rest-api.php' ); 58 | new Disable_REST_API( __FILE__ ); 59 | 60 | } 61 | 62 | unset( $dra_requirements_check ); 63 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-dinky -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | ## Welcome to GitHub Pages FRONT END MEETING 2 | 3 | You can use the [editor on GitHub](https://github.com/dmchale/disable-json-api/edit/master/docs/index.md) to maintain and preview the content for your website in Markdown files. 4 | 5 | Whenever you commit to this repository, GitHub Pages will run [Jekyll](https://jekyllrb.com/) to rebuild the pages in your site, from the content in your Markdown files. 6 | 7 | ### Markdown 8 | 9 | Markdown is a lightweight and easy-to-use syntax for styling your writing. It includes conventions for 10 | 11 | ```markdown 12 | Syntax highlighted code block 13 | 14 | # Header 1 15 | ## Header 2 16 | ### Header 3 17 | 18 | - Bulleted 19 | - List 20 | 21 | 1. Numbered 22 | 2. List 23 | 24 | **Bold** and _Italic_ and `Code` text 25 | 26 | [Link](url) and ![Image](src) 27 | ``` 28 | 29 | For more details see [Basic writing and formatting syntax](https://docs.github.com/en/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). 30 | 31 | ### Jekyll Themes 32 | 33 | Your Pages site will use the layout and styles from the Jekyll theme you have selected in your [repository settings](https://github.com/dmchale/disable-json-api/settings/pages). The name of this theme is saved in the Jekyll `_config.yml` configuration file. 34 | 35 | ### Support or Contact 36 | 37 | Having trouble with Pages? Check out our [documentation](https://docs.github.com/categories/github-pages-basics/) or [contact support](https://support.github.com/contact) and we’ll help you sort it out. 38 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | { 3 | el.addEventListener( 'click', () => { 4 | el.style.display = new_display; 5 | }); 6 | }); 7 | } 8 | 9 | function dra_maybe_show_routes() { 10 | let manage = document.querySelector('input[name=default_allow]:checked').value.toString(); 11 | if ( '0' === manage ) { 12 | dra_set_route_display( 'block' ); 13 | } else { 14 | dra_set_route_display( 'none' ); 15 | } 16 | } 17 | 18 | document.addEventListener( 'DOMContentLoaded', () => { 19 | 20 | dra_maybe_show_routes(); 21 | 22 | document.getElementById('dra-role').addEventListener( 'change', function() { 23 | window.location.href = window.location.origin + window.location.pathname + '?page=disable_rest_api_settings&role=' + this.value; 24 | }); 25 | 26 | document.querySelectorAll('input[name=default_allow]').forEach( el => { 27 | el.addEventListener( 'change', () => { 28 | dra_maybe_show_routes(); 29 | }); 30 | }); 31 | 32 | }); 33 | -------------------------------------------------------------------------------- /js/admin-header.js: -------------------------------------------------------------------------------- 1 | function dra_namespace_click(namespace, id) { 2 | if (document.getElementById('#dra_namespace_' + id).checked) { 3 | document.getElementById("#route-container input[data-namespace='" + namespace + "']").checked = true; 4 | } else { 5 | document.getElementById("#route-container input[data-namespace='" + namespace + "']").checked = false; 6 | } 7 | } -------------------------------------------------------------------------------- /languages/index.php: -------------------------------------------------------------------------------- 1 | Add New' page in your WordPress admin area 29 | 1. Activate the plugin through the 'Plugins' menu in WordPress 30 | 31 | == Frequently Asked Questions == 32 | 33 | = How do I know if this plugin is working? = 34 | 35 | While logged into WordPress as any user, the REST API will function as intended. Because of this, you must use a new browser - or Chrome's incognito mode - to test your website with a clean session. Go to yourdomain.com/wp-json/ (or yourdomain.com/?rest_route=/ if you have pretty permalinks disabled) while NOT LOGGED IN to test the results. You will see an authentication error returned if the plugin is active. "DRA: Only authenticated users can access the REST API." 36 | 37 | = Does this plugin disable every REST API that is installed on my site? = 38 | 39 | This plugin is ONLY meant to disable endpoints accessible via the core REST API that is part of WordPress itself. If a plugin or theme has implemented their own REST API (not to be confused with implementing their own endpoints within the WordPress API) this plugin will have no effect. 40 | 41 | == Screenshots == 42 | 43 | 1. The JSON returned by a website with the API disabled via filters (WP versions 4.4, 4.5, 4.6) 44 | 2. The JSON returned by a website with the API disabled via authentication methods (WP versions 4.7+) 45 | 3. The Settings page lets you selectively whitelist endpoints registered with the REST API, on a per-user-role basis. 46 | 47 | == Changelog == 48 | 49 | = 1.8 = 50 | * Tested up to WP v6.3 51 | * Added `dra_error_message` filter so devs can customize the access error message 52 | * Fixed bug that caused fatal errors if activating plugin on installations running the LearnPress plugin 53 | * Changed minimum requirements to PHP 5.6 (up from 5.3) and WordPress 4.9 (up from 4.4). Adding docblock comments to support minimums. 54 | 55 | = 1.7 = 56 | * Tested up to WP v5.8 57 | * Replace use of filemtime() with plugin version number for static file enqueues. Props @tangrufus for bringing this up! 58 | * Fixed logic bug for role-based default_allow rules. Props @msp1974 for the report! 59 | * Few small code-style updates 60 | 61 | = 1.6 = 62 | * Tested up to WP v5.6 63 | * Added support for managing endpoint access on a per-user-role basis 64 | * Soooooooo many small changes behind the scenes to support the above 65 | 66 | = 1.5.1 = 67 | * Tested up to WP v5.5 68 | 69 | = 1.5 = 70 | * Tested up to WP v5.3 71 | * Added enforcement for WordPress and PHP minimum version requirements 72 | * Fixed minor bug to prevent unintended empty routes 73 | * Minor text updates and adding textdomain to translation functions that didn't have them 74 | 75 | = 1.4.3 = 76 | * Added `load_plugin_textdomain()` for i18n 77 | 78 | = 1.4.2 = 79 | * Fixed issue causing unintentional unlocking of endpoints when another WP_Error existed before this plugin did its job 80 | 81 | = 1.4.1 = 82 | * Fixed echo of text URL to primary Plugins page in WP Dashboard 83 | 84 | = 1.4 = 85 | * Tested for WP v4.8 86 | * Tested for PHP 5.3+ 87 | * Added settings screen 88 | * Site Admins may now whitelist routes that they wish to allow unauthenticated access to 89 | * Added `dra_allow_rest_api` filter to the is_logged_in() check, so developers can get more granular with permissions 90 | * Props to @tangrufus for all of the help that went into this release 91 | 92 | = 1.3 = 93 | * Tested for WP v4.7 94 | * Adding new functionality to raise authentication errors in 4.7+ for non-logged-in users 95 | 96 | = 1.2 = 97 | * Tested for WP v4.5 98 | * Removal of actions which publish REST info to the head and header 99 | 100 | = 1.1 = 101 | * Updated to support the new filters created in the 2.0 beta API 102 | 103 | = 1.0 = 104 | * Initial Release 105 | 106 | == Upgrade Notice == 107 | 108 | = 1.8 = 109 | * Improved UI/UX of admin settings page to better manage routes 110 | 111 | = 1.6 = 112 | * By popular request... now with User Role support! 113 | 114 | = 1.4 = 115 | * Adds support to optionally whitelist individual routes of the REST API via Settings page. 116 | 117 | = 1.1 = 118 | * Now with support for the 2.0 beta API filters 119 | 120 | = 1.0 = 121 | * Initial Release 122 | -------------------------------------------------------------------------------- /uninstall.php: -------------------------------------------------------------------------------- 1 |