siteguard-admin-filter.php
3 months ago
siteguard-base.php
3 weeks ago
siteguard-captcha.php
1 week ago
siteguard-config.php
1 year ago
siteguard-disable-author-query.php
2 months ago
siteguard-disable-pingback.php
3 months ago
siteguard-disable-xmlrpc.php
3 months ago
siteguard-htaccess.php
3 weeks ago
siteguard-login-alert.php
3 months ago
siteguard-login-history.php
2 months ago
siteguard-login-lock.php
2 months ago
siteguard-rename-login.php
1 week ago
siteguard-updates-notify.php
1 week ago
siteguard-waf-exclude-rule.php
3 months ago
siteguard-captcha.php
381 lines
| 1 | <?php |
| 2 | |
| 3 | require_once SITEGUARD_PATH . 'really-simple-captcha/siteguard-really-simple-captcha.php'; |
| 4 | |
| 5 | class SiteGuard_CAPTCHA extends SiteGuard_Base { |
| 6 | protected $captcha; |
| 7 | protected $prefix; |
| 8 | protected $word; |
| 9 | |
| 10 | private $last_check_result = null; |
| 11 | private $last_check_prefix = null; |
| 12 | |
| 13 | /** |
| 14 | * Whether this PHP can render a CAPTCHA image at this moment. |
| 15 | * |
| 16 | * The requirements are checked on activation and on the settings page, but |
| 17 | * the environment can change afterwards: PHP is updated, the site is moved, |
| 18 | * GD is rebuilt without FreeType, the functions are listed in |
| 19 | * disable_functions -- or the plugin was activated through WP-CLI, whose SAPI |
| 20 | * has GD while php-fpm does not. The stored setting then says the feature is |
| 21 | * on while generate_image() would be a call to an undefined function, taking |
| 22 | * the login page down with a 500. So the capability is checked at run time, |
| 23 | * every request, right where the hooks are registered. The three functions |
| 24 | * are the ones generate_image() cannot do without. |
| 25 | */ |
| 26 | public static function is_image_rendering_available() { |
| 27 | return function_exists( 'imagecreatetruecolor' ) |
| 28 | && function_exists( 'imagettftext' ) |
| 29 | && function_exists( 'imagepng' ); |
| 30 | } |
| 31 | |
| 32 | /** |
| 33 | * Whether the image and answer files can actually be written. |
| 34 | * |
| 35 | * Activating through WP-CLI as a different user than the web server (root is |
| 36 | * the usual case) creates wp-content/siteguard/ owned by that user, and the |
| 37 | * web server can then create nothing inside it. The image write fails, the |
| 38 | * answer file is never written, and every response is rejected -- the login |
| 39 | * page becomes impossible to pass. make_tmp_dir() used to report success in |
| 40 | * that state because the directory already existed, so neither activation nor |
| 41 | * the settings page noticed. |
| 42 | * |
| 43 | * The directory is created on first use, so until it exists the question is |
| 44 | * whether it can be created, which is a property of the parent. Memoized: the |
| 45 | * answer cannot change within a request, and this is on the front-end path. |
| 46 | */ |
| 47 | public static function is_answer_dir_writable() { |
| 48 | static $writable = null; |
| 49 | if ( null !== $writable ) { |
| 50 | return $writable; |
| 51 | } |
| 52 | $dir = SiteGuardReallySimpleCaptcha::get_tmp_dir(); |
| 53 | $writable = is_dir( $dir ) ? is_writable( $dir ) : is_writable( WP_CONTENT_DIR ); |
| 54 | return $writable; |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Whether a CAPTCHA can be produced and verified at all right now. |
| 59 | * |
| 60 | * Both halves have to hold together: presenting a CAPTCHA that cannot be |
| 61 | * checked, or demanding one that cannot be drawn, locks everyone out of the |
| 62 | * login page. This is the single answer used by the hook registration, by the |
| 63 | * login URL rescue form and by the requirement check on the settings page, so |
| 64 | * those three can never disagree with each other. |
| 65 | */ |
| 66 | public static function is_captcha_available() { |
| 67 | return self::is_image_rendering_available() && self::is_answer_dir_writable(); |
| 68 | } |
| 69 | |
| 70 | function __construct() { |
| 71 | global $siteguard_config; |
| 72 | // The capability test belongs to this condition and not to any single |
| 73 | // handler: the form and its verification are registered together, so a |
| 74 | // server that cannot draw the image simply gets no CAPTCHA instead of a |
| 75 | // form that demands characters nobody can read. captcha_enable is left |
| 76 | // alone on purpose -- a temporary state of the environment is not a |
| 77 | // reason to rewrite a stored setting, and the feature comes back by |
| 78 | // itself once the server can render again. |
| 79 | if ( '1' == $siteguard_config->get( 'captcha_enable' ) && 'xmlrpc.php' != basename( $_SERVER['SCRIPT_NAME'] ) && ! is_admin() && self::is_captcha_available() ) { |
| 80 | $this->captcha = new SiteGuardReallySimpleCaptcha(); |
| 81 | |
| 82 | add_filter( 'shake_error_codes', array( $this, 'handler_shake_error_codes' ) ); |
| 83 | |
| 84 | // for login |
| 85 | if ( '0' !== $siteguard_config->get( 'captcha_login' ) ) { |
| 86 | add_filter( 'login_form', array( $this, 'handler_login_form' ) ); |
| 87 | add_filter( 'wp_authenticate_user', array( $this, 'handler_wp_authenticate_user' ), 1, 2 ); |
| 88 | } |
| 89 | // for lost password |
| 90 | if ( '0' !== $siteguard_config->get( 'captcha_lostpasswd' ) ) { |
| 91 | add_filter( 'lostpassword_form', array( $this, 'handler_lostpassword_form' ) ); |
| 92 | add_filter( 'lostpassword_post', array( $this, 'handler_lostpassword_post' ), 1 ); |
| 93 | } |
| 94 | // for register user |
| 95 | if ( '0' !== $siteguard_config->get( 'captcha_registuser' ) ) { |
| 96 | add_filter( 'register_form', array( $this, 'handler_register_form' ) ); |
| 97 | add_action( 'registration_errors', array( $this, 'handler_registration_errors' ), 10, 3 ); |
| 98 | } |
| 99 | // for comment |
| 100 | if ( '0' !== $siteguard_config->get( 'captcha_comment' ) ) { |
| 101 | add_action( 'comment_form_after_fields', array( $this, 'handler_comment_form' ), 1 ); |
| 102 | add_action( 'comment_form_logged_in_after', array( $this, 'handler_comment_form' ), 1 ); |
| 103 | add_action( 'comment_form', array( $this, 'handler_comment_form' ) ); |
| 104 | add_filter( 'preprocess_comment', array( $this, 'handler_process_comment_post' ) ); |
| 105 | add_action( 'wp_footer', array( $this, 'comment_captcha_reload_script' ) ); |
| 106 | } |
| 107 | } |
| 108 | if ( '1' == $siteguard_config->get( 'same_login_error' ) ) { |
| 109 | add_filter( 'login_errors', array( $this, 'handler_login_errors' ) ); |
| 110 | } |
| 111 | // A security feature must not switch itself off quietly. |
| 112 | if ( is_admin() && '1' == $siteguard_config->get( 'captcha_enable' ) && ! self::is_captcha_available() ) { |
| 113 | add_action( 'admin_notices', array( $this, 'handler_admin_notices_captcha_unavailable' ) ); |
| 114 | } |
| 115 | } |
| 116 | |
| 117 | function handler_admin_notices_captcha_unavailable() { |
| 118 | if ( ! current_user_can( 'manage_options' ) ) { |
| 119 | return; |
| 120 | } |
| 121 | // The two causes need different answers: one is for the hosting provider, |
| 122 | // the other the administrator can fix, so name the directory. |
| 123 | if ( ! self::is_image_rendering_available() ) { |
| 124 | $message = esc_html__( 'SiteGuard WP Plugin: CAPTCHA is turned on, but this server cannot render the CAPTCHA image, so CAPTCHA is not being applied. Please contact your hosting provider about the required image rendering support.', 'siteguard' ); |
| 125 | } else { |
| 126 | $message = sprintf( |
| 127 | /* translators: %s: Filesystem path of the CAPTCHA working directory. */ |
| 128 | esc_html__( 'SiteGuard WP Plugin: CAPTCHA is turned on, but the directory %s cannot be written to by the web server, so CAPTCHA is not being applied. Please change the owner of this directory to the user the web server runs as.', 'siteguard' ), |
| 129 | esc_html( SiteGuardReallySimpleCaptcha::get_tmp_dir() ) |
| 130 | ); |
| 131 | } |
| 132 | echo '<div class="notice notice-warning is-dismissible"><p>'; |
| 133 | echo $message; |
| 134 | echo ' '; |
| 135 | echo esc_html__( 'The setting has been left as it is, and CAPTCHA resumes automatically once the problem is resolved.', 'siteguard' ); |
| 136 | echo '</p></div>'; |
| 137 | } |
| 138 | private function check_captcha_with_cache() { |
| 139 | $current_prefix = isset( $_POST['siteguard_captcha_prefix'] ) ? $_POST['siteguard_captcha_prefix'] : ''; |
| 140 | if ( $this->last_check_prefix !== $current_prefix ) { |
| 141 | $this->last_check_result = null; |
| 142 | $this->last_check_prefix = $current_prefix; |
| 143 | } |
| 144 | if ( null !== $this->last_check_result ) { |
| 145 | return $this->last_check_result; |
| 146 | } |
| 147 | |
| 148 | $is_ok = false; |
| 149 | if ( array_key_exists( 'siteguard_captcha', $_POST ) && array_key_exists( 'siteguard_captcha_prefix', $_POST ) ) { |
| 150 | $is_ok = $this->captcha->check( $_POST['siteguard_captcha_prefix'], $_POST['siteguard_captcha'], true ); |
| 151 | } |
| 152 | |
| 153 | $this->last_check_result = $is_ok; |
| 154 | return $is_ok; |
| 155 | } |
| 156 | function check_requirements() { |
| 157 | $error = siteguard_check_multisite(); |
| 158 | if ( is_wp_error( $error ) ) { |
| 159 | return $error; |
| 160 | } |
| 161 | $error = $this->check_extensions(); |
| 162 | if ( is_wp_error( $error ) ) { |
| 163 | return $error; |
| 164 | } |
| 165 | $error = $this->check_image_access(); |
| 166 | if ( is_wp_error( $error ) ) { |
| 167 | return $error; |
| 168 | } |
| 169 | $error = $this->check_support_freetype(); |
| 170 | if ( is_wp_error( $error ) ) { |
| 171 | return $error; |
| 172 | } |
| 173 | return true; |
| 174 | } |
| 175 | |
| 176 | function check_extensions() { |
| 177 | $error_extensions = array(); |
| 178 | $extensions = array( |
| 179 | 'mbstring', |
| 180 | 'gd', |
| 181 | ); |
| 182 | foreach ( $extensions as $extension ) { |
| 183 | if ( ! extension_loaded( $extension ) ) { |
| 184 | $error_extensions[] = $extension; |
| 185 | } |
| 186 | } |
| 187 | if ( empty( $error_extensions ) ) { |
| 188 | return true; |
| 189 | } |
| 190 | |
| 191 | $message = esc_html__( 'This feature requires additional server components. Please contact your hosting provider to enable it.', 'siteguard' ); |
| 192 | |
| 193 | $error = new WP_Error( 'siteguard_captcha', $message ); |
| 194 | return $error; |
| 195 | } |
| 196 | |
| 197 | function check_image_access() { |
| 198 | if ( is_object( $this->captcha ) ) { |
| 199 | $ret = $this->captcha->make_tmp_dir(); |
| 200 | } else { |
| 201 | $captcha = new SiteGuardReallySimpleCaptcha(); |
| 202 | $ret = $captcha->make_tmp_dir(); |
| 203 | } |
| 204 | if ( false === $ret ) { |
| 205 | $message = esc_html__( 'Failed to write the CAPTCHA image file.', 'siteguard' ); |
| 206 | $error = new WP_Error( 'siteguard_captcha', $message ); |
| 207 | return $error; |
| 208 | } |
| 209 | |
| 210 | return true; |
| 211 | } |
| 212 | |
| 213 | function check_support_freetype() { |
| 214 | // The same three functions the run-time gate uses, so that the settings |
| 215 | // page cannot report the feature as usable while it is being skipped on |
| 216 | // the login page. imagettftext() alone missed a server that has GD and |
| 217 | // FreeType but hides imagepng() behind disable_functions. |
| 218 | if ( self::is_image_rendering_available() ) { |
| 219 | return true; |
| 220 | } |
| 221 | $message = esc_html__( 'Your server does not support the image rendering required for this feature. Please contact your hosting provider.', 'siteguard' ); |
| 222 | $error = new WP_Error( 'siteguard_captcha', $message ); |
| 223 | return $error; |
| 224 | } |
| 225 | |
| 226 | function handler_login_errors( $error ) { |
| 227 | if ( strlen( $error ) > 0 && false === strpos( $error, esc_html__( 'ERROR: LOGIN LOCKED', 'siteguard' ) ) ) { |
| 228 | $error = esc_html__( 'ERROR: Please check your input and try again.', 'siteguard' ); |
| 229 | } |
| 230 | return $error; |
| 231 | } |
| 232 | |
| 233 | function handler_shake_error_codes( $shake_error_codes ) { |
| 234 | array_push( $shake_error_codes, 'siteguard-captcha-error' ); |
| 235 | return $shake_error_codes; |
| 236 | } |
| 237 | |
| 238 | function init() { |
| 239 | global $siteguard_config; |
| 240 | $errors = $this->check_requirements(); |
| 241 | if ( ! is_wp_error( $errors ) ) { |
| 242 | $switch = '1'; |
| 243 | } else { |
| 244 | $switch = '0'; |
| 245 | } |
| 246 | $siteguard_config->set( 'captcha_enable', $switch ); |
| 247 | |
| 248 | $language = get_bloginfo( 'language' ); |
| 249 | if ( 'ja' == $language ) { |
| 250 | $mode = '1'; // hiragana |
| 251 | } else { |
| 252 | $mode = '2'; // alphanumeric |
| 253 | } |
| 254 | $siteguard_config->set( 'captcha_login', $mode ); |
| 255 | $siteguard_config->set( 'captcha_comment', $mode ); |
| 256 | $siteguard_config->set( 'captcha_lostpasswd', $mode ); |
| 257 | $siteguard_config->set( 'captcha_registuser', $mode ); |
| 258 | |
| 259 | if ( true === siteguard_check_multisite() ) { |
| 260 | $siteguard_config->set( 'same_login_error', '1' ); |
| 261 | } else { |
| 262 | $siteguard_config->set( 'same_login_error', '0' ); |
| 263 | } |
| 264 | $siteguard_config->update(); |
| 265 | } |
| 266 | |
| 267 | function get_captcha() { |
| 268 | $result = '<p>'; |
| 269 | $result .= '<img src="' . WP_CONTENT_URL . '/siteguard/' . $this->prefix . '.png" alt="CAPTCHA">'; |
| 270 | $result .= '</p><p>'; |
| 271 | $result .= '<label for="siteguard_captcha">' . esc_html__( 'Please enter the characters shown above.', 'siteguard' ) . '</label><br />'; |
| 272 | $result .= '<input type="text" name="siteguard_captcha" id="siteguard_captcha" class="input" value="" size="10" aria-required="true" />'; |
| 273 | $result .= '<input type="hidden" name="siteguard_captcha_prefix" id="siteguard_captcha_prefix" value="' . $this->prefix . '" />'; |
| 274 | $result .= '</p>'; |
| 275 | |
| 276 | return $result; |
| 277 | } |
| 278 | |
| 279 | function put_captcha() { |
| 280 | $this->word = $this->captcha->generate_random_word(); |
| 281 | $this->prefix = siteguard_rand(); |
| 282 | $this->captcha->generate_image( $this->prefix, $this->word ); |
| 283 | echo $this->get_captcha(); |
| 284 | } |
| 285 | |
| 286 | function handler_login_form() { |
| 287 | global $siteguard_config; |
| 288 | ( '2' === $siteguard_config->get( 'captcha_login' ) ) ? $this->captcha->set_lang_mode( 'en' ) : $this->captcha->set_lang_mode( 'jp' ); |
| 289 | $this->put_captcha(); |
| 290 | } |
| 291 | |
| 292 | function handler_comment_form( $post_id ) { |
| 293 | global $siteguard_config; |
| 294 | if ( defined( 'SITEGUARD_PUT_COMMENT_FORM' ) ) { |
| 295 | return; |
| 296 | } |
| 297 | ( '2' === $siteguard_config->get( 'captcha_comment' ) ) ? $this->captcha->set_lang_mode( 'en' ) : $this->captcha->set_lang_mode( 'jp' ); |
| 298 | $this->put_captcha(); |
| 299 | define( 'SITEGUARD_PUT_COMMENT_FORM', '1' ); |
| 300 | } |
| 301 | |
| 302 | function handler_lostpassword_form() { |
| 303 | global $siteguard_config; |
| 304 | ( '2' === $siteguard_config->get( 'captcha_lostpasswd' ) ) ? $this->captcha->set_lang_mode( 'en' ) : $this->captcha->set_lang_mode( 'jp' ); |
| 305 | $this->put_captcha(); |
| 306 | } |
| 307 | |
| 308 | function handler_register_form() { |
| 309 | global $siteguard_config; |
| 310 | ( '2' == $siteguard_config->get( 'captcha_registuser' ) ) ? $this->captcha->set_lang_mode( 'en' ) : $this->captcha->set_lang_mode( 'jp' ); |
| 311 | $this->put_captcha(); |
| 312 | } |
| 313 | |
| 314 | function handler_wp_authenticate_user( $user, $password ) { |
| 315 | if ( array_key_exists( 'siteguard_captcha', $_POST ) && array_key_exists( 'siteguard_captcha_prefix', $_POST ) ) { |
| 316 | if ( $this->check_captcha_with_cache() ) { |
| 317 | return $user; |
| 318 | } |
| 319 | } |
| 320 | $error = new WP_Error(); |
| 321 | $error->add( 'siteguard-captcha-error', esc_html__( 'ERROR: Invalid CAPTCHA.', 'siteguard' ) ); |
| 322 | return $error; |
| 323 | } |
| 324 | |
| 325 | function add_captcha_error() { |
| 326 | return new WP_Error( 'siteguard-captcha-error', esc_html__( 'ERROR: Invalid CAPTCHA.', 'siteguard' ) ); |
| 327 | } |
| 328 | |
| 329 | function handler_lostpassword_post() { |
| 330 | if ( array_key_exists( 'siteguard_captcha', $_POST ) && array_key_exists( 'siteguard_captcha_prefix', $_POST ) ) { |
| 331 | if ( $this->check_captcha_with_cache() ) { |
| 332 | return; |
| 333 | } |
| 334 | } |
| 335 | add_filter( 'allow_password_reset', array( $this, 'add_captcha_error' ) ); |
| 336 | } |
| 337 | |
| 338 | function handler_registration_errors( $errors, $sanitized_user_login, $user_email ) { |
| 339 | if ( array_key_exists( 'siteguard_captcha', $_POST ) && array_key_exists( 'siteguard_captcha_prefix', $_POST ) ) { |
| 340 | if ( $this->check_captcha_with_cache() ) { |
| 341 | return $errors; |
| 342 | } |
| 343 | } |
| 344 | $new_errors = new WP_Error(); |
| 345 | $new_errors->add( 'siteguard-captcha-error', esc_html__( 'ERROR: Invalid CAPTCHA.', 'siteguard' ) ); |
| 346 | return $new_errors; |
| 347 | } |
| 348 | |
| 349 | function handler_process_comment_post( $comment ) { |
| 350 | if ( is_admin() ) { |
| 351 | return $comment; |
| 352 | } |
| 353 | if ( array_key_exists( 'siteguard_captcha', $_POST ) && array_key_exists( 'siteguard_captcha_prefix', $_POST ) ) { |
| 354 | if ( ! empty( $_POST['siteguard_captcha'] ) ) { |
| 355 | if ( $this->check_captcha_with_cache( ) ) { |
| 356 | return $comment; |
| 357 | } |
| 358 | } |
| 359 | } |
| 360 | wp_die( esc_html__( 'ERROR: Invalid CAPTCHA.', 'siteguard' ), esc_html( 'ERROR'), array( 'back_link' => true ) ); |
| 361 | } |
| 362 | public function comment_captcha_reload_script( ) { |
| 363 | if ( is_singular() && comments_open() ) { |
| 364 | ?> |
| 365 | <script> |
| 366 | window.addEventListener('pageshow', function(event) { |
| 367 | var isBackForward = false; |
| 368 | if (window.performance && typeof performance.getEntriesByType === 'function') { |
| 369 | var perfEntries = performance.getEntriesByType('navigation'); |
| 370 | isBackForward = perfEntries.length > 0 && perfEntries[0].type === 'back_forward'; |
| 371 | } |
| 372 | if (event.persisted || isBackForward) { |
| 373 | window.location.reload(); |
| 374 | } |
| 375 | }); |
| 376 | </script> |
| 377 | <?php |
| 378 | } |
| 379 | } |
| 380 | } |
| 381 |