| @@ -1,67 +1,148 @@ | ||
| 1 | -<?php | |
| 2 | -// Exit if accessed directly. | |
| 3 | -if ( ! defined( 'ABSPATH' ) ) { | |
| 4 | - exit; | |
| 5 | -} | |
| 6 | - | |
| 7 | -/** | |
| 8 | - * Disable XML-RPC and block direct access to xmlrpc.php. | |
| 9 | - * | |
| 10 | - * If the "Disable XML-RPC" option is enabled, this function disables the XML-RPC functionality, | |
| 11 | - * blocks direct access to xmlrpc.php by sending a 403 Forbidden header, and modifies the .htaccess file | |
| 12 | - * to deny access to xmlrpc.php. If the option is disabled, any previously added block is removed. | |
| 13 | - * | |
| 14 | - * @return void | |
| 15 | - */ | |
| 16 | -function ns_shield_disable_xml_rpc() { | |
| 17 | - global $wp_filesystem; | |
| 18 | - | |
| 19 | - // Initialize the filesystem if not yet set up. | |
| 20 | - if ( empty( $wp_filesystem ) ) { | |
| 21 | - require_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 22 | - WP_Filesystem(); | |
| 23 | - } | |
| 24 | - | |
| 25 | - // Check if the "Disable XML-RPC" switch is enabled. | |
| 26 | - $is_xmlrpc_disabled = get_option( 'ns_shield_xml_rpc', false ); | |
| 27 | - | |
| 28 | - if ( $is_xmlrpc_disabled ) { | |
| 29 | - // Disable XML-RPC functionality. | |
| 30 | - add_filter( 'xmlrpc_enabled', '__return_false' ); | |
| 31 | - | |
| 32 | - // Block direct access to xmlrpc.php. | |
| 33 | - add_action( 'init', function() { | |
| 34 | - if ( isset( $_SERVER['REQUEST_URI'] ) && strpos( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 'xmlrpc.php' ) !== false ) { | |
| 35 | - header( 'HTTP/1.1 403 Forbidden' ); | |
| 36 | - $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; | |
| 37 | - if ( stripos( $user_agent, 'curl' ) !== false ) { | |
| 38 | - exit( 'Protected by Netsensai Shield' ); | |
| 39 | - } else { | |
| 40 | - exit( 'XML-RPC is disabled on this site.' ); | |
| 41 | - } | |
| 42 | - } | |
| 43 | - } ); | |
| 44 | - | |
| 45 | - // Edit .htaccess to block xmlrpc.php. | |
| 46 | - $htaccess_file = ABSPATH . '.htaccess'; | |
| 47 | - $htaccess_code = "<Files xmlrpc.php>\n order deny,allow\n deny from all\n</Files>\n"; | |
| 48 | - | |
| 49 | - if ( $wp_filesystem->exists( $htaccess_file ) && $wp_filesystem->is_writable( $htaccess_file ) ) { | |
| 50 | - $htaccess_content = $wp_filesystem->get_contents( $htaccess_file ); | |
| 51 | - if ( strpos( $htaccess_content, 'xmlrpc.php' ) === false ) { | |
| 52 | - $wp_filesystem->put_contents( $htaccess_file, $htaccess_code . $htaccess_content, FS_CHMOD_FILE ); | |
| 53 | - } | |
| 54 | - } | |
| 55 | - } else { | |
| 56 | - // If the switch is disabled, remove any xmlrpc.php block from .htaccess. | |
| 57 | - $htaccess_file = ABSPATH . '.htaccess'; | |
| 58 | - $htaccess_code = "<Files xmlrpc.php>\n order deny,allow\n deny from all\n</Files>\n"; | |
| 59 | - | |
| 60 | - if ( $wp_filesystem->exists( $htaccess_file ) && $wp_filesystem->is_writable( $htaccess_file ) ) { | |
| 61 | - $htaccess_content = $wp_filesystem->get_contents( $htaccess_file ); | |
| 62 | - $updated_content = str_replace( $htaccess_code, '', $htaccess_content ); | |
| 63 | - $wp_filesystem->put_contents( $htaccess_file, $updated_content, FS_CHMOD_FILE ); | |
| 64 | - } | |
| 65 | - } | |
| 66 | -} | |
| 67 | -add_action( 'init', 'ns_shield_disable_xml_rpc' ); | |
| 1 | +<?php | |
| 2 | +// Exit if accessed directly. | |
| 3 | +if ( ! defined( 'ABSPATH' ) ) { | |
| 4 | + exit; | |
| 5 | +} | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * Return whether an option value represents a checked WordPress checkbox. | |
| 9 | + * | |
| 10 | + * @param mixed $value Option value. | |
| 11 | + * @return bool | |
| 12 | + */ | |
| 13 | +function ns_shield_htaccess_option_is_enabled( $value ) { | |
| 14 | + return true === $value || 1 === $value || '1' === $value; | |
| 15 | +} | |
| 16 | + | |
| 17 | +/** | |
| 18 | + * Store and return a .htaccess synchronization result for later admin feedback. | |
| 19 | + * | |
| 20 | + * @param string $option Result option name. | |
| 21 | + * @param bool $success Whether the operation succeeded. | |
| 22 | + * @param string $code Machine-readable result code. | |
| 23 | + * @return array | |
| 24 | + */ | |
| 25 | +function ns_shield_htaccess_sync_result( $option, $success, $code ) { | |
| 26 | + $result = array( | |
| 27 | + 'success' => (bool) $success, | |
| 28 | + 'code' => sanitize_key( $code ), | |
| 29 | + 'time' => time(), | |
| 30 | + ); | |
| 31 | + | |
| 32 | + update_option( $option, $result, false ); | |
| 33 | + | |
| 34 | + return $result; | |
| 35 | +} | |
| 36 | + | |
| 37 | +/** | |
| 38 | + * Synchronize one WordPress-managed .htaccess marker section. | |
| 39 | + * | |
| 40 | + * insert_with_markers() locks the file while updating its own marker section, | |
| 41 | + * leaving WordPress, administrator, and other-plugin rules untouched. | |
| 42 | + * | |
| 43 | + * @param string $marker Marker name without BEGIN or END. | |
| 44 | + * @param string[] $lines Lines to insert, or an empty array to clear them. | |
| 45 | + * @param string $result_option Option used to store the diagnostic result. | |
| 46 | + * @return array | |
| 47 | + */ | |
| 48 | +function ns_shield_sync_htaccess_marker( $marker, $lines, $result_option ) { | |
| 49 | + $htaccess_file = ABSPATH . '.htaccess'; | |
| 50 | + | |
| 51 | + if ( ! file_exists( $htaccess_file ) ) { | |
| 52 | + return ns_shield_htaccess_sync_result( $result_option, false, 'htaccess_missing' ); | |
| 53 | + } | |
| 54 | + | |
| 55 | + if ( ! wp_is_writable( $htaccess_file ) ) { | |
| 56 | + return ns_shield_htaccess_sync_result( $result_option, false, 'htaccess_not_writable' ); | |
| 57 | + } | |
| 58 | + | |
| 59 | + if ( ! function_exists( 'insert_with_markers' ) ) { | |
| 60 | + require_once ABSPATH . 'wp-admin/includes/misc.php'; | |
| 61 | + } | |
| 62 | + | |
| 63 | + if ( ! function_exists( 'insert_with_markers' ) ) { | |
| 64 | + return ns_shield_htaccess_sync_result( $result_option, false, 'markers_unavailable' ); | |
| 65 | + } | |
| 66 | + | |
| 67 | + if ( ! insert_with_markers( $htaccess_file, $marker, $lines ) ) { | |
| 68 | + return ns_shield_htaccess_sync_result( $result_option, false, 'htaccess_write_failed' ); | |
| 69 | + } | |
| 70 | + | |
| 71 | + return ns_shield_htaccess_sync_result( $result_option, true, 'updated' ); | |
| 72 | +} | |
| 73 | + | |
| 74 | +/** | |
| 75 | + * Disable XML-RPC at runtime without touching .htaccess. | |
| 76 | + * | |
| 77 | + * @return void | |
| 78 | + */ | |
| 79 | +function ns_shield_disable_xml_rpc() { | |
| 80 | + if ( ! ns_shield_htaccess_option_is_enabled( get_option( 'ns_shield_xml_rpc', false ) ) ) { | |
| 81 | + return; | |
| 82 | + } | |
| 83 | + | |
| 84 | + add_filter( 'xmlrpc_enabled', '__return_false' ); | |
| 85 | + | |
| 86 | + if ( isset( $_SERVER['REQUEST_URI'] ) && false !== strpos( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), 'xmlrpc.php' ) ) { | |
| 87 | + header( 'HTTP/1.1 403 Forbidden' ); | |
| 88 | + $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; | |
| 89 | + if ( false !== stripos( $user_agent, 'curl' ) ) { | |
| 90 | + exit( 'Protected by Netsensai Shield' ); | |
| 91 | + } | |
| 92 | + | |
| 93 | + exit( 'XML-RPC is disabled on this site.' ); | |
| 94 | + } | |
| 95 | +} | |
| 96 | +add_action( 'init', 'ns_shield_disable_xml_rpc' ); | |
| 97 | + | |
| 98 | +/** | |
| 99 | + * Synchronize the XML-RPC marker section after an option save. | |
| 100 | + * | |
| 101 | + * @param mixed $value New option value. | |
| 102 | + * @return array | |
| 103 | + */ | |
| 104 | +function ns_shield_sync_xml_rpc_htaccess( $value ) { | |
| 105 | + $lines = ns_shield_htaccess_option_is_enabled( $value ) | |
| 106 | + ? array( | |
| 107 | + '<Files xmlrpc.php>', | |
| 108 | + ' order deny,allow', | |
| 109 | + ' deny from all', | |
| 110 | + '</Files>', | |
| 111 | + ) | |
| 112 | + : array(); | |
| 113 | + | |
| 114 | + return ns_shield_sync_htaccess_marker( | |
| 115 | + 'NETSENSAI SHIELD XML-RPC', | |
| 116 | + $lines, | |
| 117 | + 'ns_shield_xml_rpc_htaccess_sync_result' | |
| 118 | + ); | |
| 119 | +} | |
| 120 | + | |
| 121 | +/** | |
| 122 | + * Synchronize after the option is first added. | |
| 123 | + * | |
| 124 | + * @param string $option Option name. | |
| 125 | + * @param mixed $value New option value. | |
| 126 | + * @return void | |
| 127 | + */ | |
| 128 | +function ns_shield_xml_rpc_option_added( $option, $value ) { | |
| 129 | + ns_shield_sync_xml_rpc_htaccess( $value ); | |
| 130 | +} | |
| 131 | +add_action( 'add_option_ns_shield_xml_rpc', 'ns_shield_xml_rpc_option_added', 10, 2 ); | |
| 132 | + | |
| 133 | +/** | |
| 134 | + * Synchronize only after the option value actually changes. | |
| 135 | + * | |
| 136 | + * @param mixed $old_value Previous option value. | |
| 137 | + * @param mixed $value New option value. | |
| 138 | + * @param string $option Option name. | |
| 139 | + * @return void | |
| 140 | + */ | |
| 141 | +function ns_shield_xml_rpc_option_updated( $old_value, $value, $option ) { | |
| 142 | + if ( ns_shield_htaccess_option_is_enabled( $old_value ) === ns_shield_htaccess_option_is_enabled( $value ) ) { | |
| 143 | + return; | |
| 144 | + } | |
| 145 | + | |
| 146 | + ns_shield_sync_xml_rpc_htaccess( $value ); | |
| 147 | +} | |
| 148 | +add_action( 'update_option_ns_shield_xml_rpc', 'ns_shield_xml_rpc_option_updated', 10, 3 ); | |