admin
7 months ago
cli
8 months ago
lib
11 months ago
captcha.php
2 years ago
cloudsecure-wp.php
6 months ago
common.php
1 year ago
config.php
2 years ago
disable-access-system-file.php
1 year ago
disable-author-query.php
2 years ago
disable-login.php
1 year ago
disable-restapi.php
7 months ago
disable-xmlrpc.php
1 year ago
htaccess.php
1 year ago
login-log.php
1 year ago
login-notification.php
1 year ago
rename-login-page.php
1 year ago
restrict-admin-page.php
10 months ago
server-error-notification.php
1 year ago
two-factor-authentication.php
6 months ago
unify-messages.php
2 years ago
update-notice.php
7 months ago
waf-engine.php
7 months ago
waf.php
1 year ago
two-factor-authentication.php
840 lines
| 1 | <?php |
| 2 | |
| 3 | if ( ! defined( 'ABSPATH' ) ) { |
| 4 | exit; |
| 5 | } |
| 6 | |
| 7 | class CloudSecureWP_Two_Factor_Authentication extends CloudSecureWP_Common { |
| 8 | private const KEY_FEATURE = 'two_factor_authentication'; |
| 9 | private const OPTION_PREFIX = 'cloudsecurewp_2fa_data_'; |
| 10 | private const SESSION_EXPIRY = 300; |
| 11 | private const CLEANUP_TIMEOUT = 60; |
| 12 | private const CLEANUP_BATCH_SIZE = 1000; |
| 13 | |
| 14 | private $config; |
| 15 | |
| 16 | /** |
| 17 | * @var CloudSecureWP_Disable_Login |
| 18 | */ |
| 19 | private $disable_login; |
| 20 | |
| 21 | /** |
| 22 | * @var CloudSecureWP_Login_Log |
| 23 | */ |
| 24 | private $login_log; |
| 25 | |
| 26 | function __construct( array $info, CloudSecureWP_Config $config, CloudSecureWP_Disable_Login $disable_login, CloudSecureWP_Login_Log $login_log ) { |
| 27 | parent::__construct( $info ); |
| 28 | $this->config = $config; |
| 29 | $this->disable_login = $disable_login; |
| 30 | $this->login_log = $login_log; |
| 31 | } |
| 32 | |
| 33 | /** |
| 34 | * 機能毎のKEY取得 |
| 35 | * |
| 36 | * @return string |
| 37 | */ |
| 38 | public function get_feature_key(): string { |
| 39 | return self::KEY_FEATURE; |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * 有効無効判定 |
| 44 | * |
| 45 | * @return bool |
| 46 | */ |
| 47 | public function is_enabled(): bool { |
| 48 | return $this->config->get( $this->get_feature_key() ) === 't'; |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * 初期設定値取得 |
| 53 | * |
| 54 | * @return array |
| 55 | */ |
| 56 | public function get_default(): array { |
| 57 | return array( self::KEY_FEATURE => 'f' ); |
| 58 | } |
| 59 | |
| 60 | /** |
| 61 | * 設定値key取得 |
| 62 | */ |
| 63 | public function get_keys(): array { |
| 64 | return array( self::KEY_FEATURE ); |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * 設定値取得 |
| 69 | */ |
| 70 | public function get_settings(): array { |
| 71 | $settings = array(); |
| 72 | $keys = $this->get_keys(); |
| 73 | |
| 74 | foreach ( $keys as $key ) { |
| 75 | $settings[ $key ] = $this->config->get( $key ); |
| 76 | } |
| 77 | |
| 78 | return $settings; |
| 79 | } |
| 80 | |
| 81 | /** |
| 82 | * 設定値保存 |
| 83 | * |
| 84 | * @param array $settings |
| 85 | * |
| 86 | * @return void |
| 87 | */ |
| 88 | public function save_settings( array $settings ): void { |
| 89 | $keys = $this->get_keys(); |
| 90 | |
| 91 | foreach ( $keys as $key ) { |
| 92 | $this->config->set( $key, $settings[ $key ] ?? '' ); |
| 93 | } |
| 94 | $this->config->save(); |
| 95 | } |
| 96 | |
| 97 | /** |
| 98 | * 有効化 |
| 99 | * |
| 100 | * @return void |
| 101 | */ |
| 102 | public function activate(): void { |
| 103 | $settings = $this->get_default(); |
| 104 | $this->save_settings( $settings ); |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * 無効化 |
| 109 | * |
| 110 | * @return void |
| 111 | */ |
| 112 | public function deactivate(): void { |
| 113 | $settings = $this->get_settings(); |
| 114 | $settings[ self::KEY_FEATURE ] = 'f'; |
| 115 | $this->save_settings( $settings ); |
| 116 | } |
| 117 | |
| 118 | /** |
| 119 | * 管理画面上での有効無効判定 |
| 120 | * 2段階認証の管理画面で「変更を保存」ボタンを押下時、 |
| 121 | * is_enabled()のみを使うとデバイス登録のメニューが正しく表示されない。 |
| 122 | * |
| 123 | * @return bool |
| 124 | */ |
| 125 | public function is_enabled_on_screen(): bool { |
| 126 | if ( isset( $_POST['two_factor_authentication'] ) && ! empty( $_POST['two_factor_authentication'] ) ) { |
| 127 | return $this->check_environment() && sanitize_text_field( $_POST['two_factor_authentication'] ) === 't'; |
| 128 | } |
| 129 | |
| 130 | return $this->is_enabled(); |
| 131 | } |
| 132 | |
| 133 | /** |
| 134 | * 有効な権限グループに含まれるかどうか |
| 135 | * |
| 136 | * @param $role |
| 137 | * |
| 138 | * @return bool |
| 139 | */ |
| 140 | private function is_role_enabled( $role ): bool { |
| 141 | return in_array( $role, get_option( 'cloudsecurewp_two_factor_authentication_roles', array() ) ); |
| 142 | } |
| 143 | |
| 144 | /** |
| 145 | * 2段階認証のエラーを出力 |
| 146 | * |
| 147 | * @return void |
| 148 | */ |
| 149 | private function login_error() { |
| 150 | if ( array_key_exists( 'google_authenticator_code', $_REQUEST ) ) { |
| 151 | if ( sanitize_text_field( $_REQUEST['google_authenticator_code'] ) ) { |
| 152 | $errors = '認証コードが間違っているか、有効期限が切れています。'; |
| 153 | } else { |
| 154 | $errors = '認証コードが� |
| 155 | �力されていません。'; |
| 156 | } |
| 157 | echo '<div id="login_error">' . esc_html( apply_filters( 'login_errors', $errors ) ) . "</div>\n"; |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | /** |
| 162 | * 2段階認証のログインフォームを出力 |
| 163 | * |
| 164 | * @param string $login_token |
| 165 | * |
| 166 | * @return void |
| 167 | */ |
| 168 | private function login_form( $login_token ) { |
| 169 | ?> |
| 170 | <form name="loginform" id="loginform" |
| 171 | action="<?php echo esc_url( site_url( 'wp-login.php', 'login_post' ) ); ?>" method="post"> |
| 172 | <?php if ( array_key_exists( 'rememberme', $_REQUEST ) && 'forever' === sanitize_text_field( $_REQUEST['rememberme'] ) ) : ?> |
| 173 | <input name="rememberme" type="hidden" id="rememberme" value="forever"/> |
| 174 | <?php endif; ?> |
| 175 | <input type="hidden" name="login_token" value="<?php echo esc_attr( $login_token ); ?>"> |
| 176 | <p> |
| 177 | <label for="google_authenticator_code">認証コード</label> |
| 178 | <input type="text" name="google_authenticator_code" id="google_authenticator_code" class="input" |
| 179 | value="" size="20" autocomplete="one-time-code"/> |
| 180 | </p> |
| 181 | <script type="text/javascript">document.getElementById("google_authenticator_code").focus();</script> |
| 182 | <p>デバイスのGoogle Authenticator アプリケーションに表示されている6桁の認証コードを� |
| 183 | �力してください。</p> |
| 184 | <p class="submit"> |
| 185 | <?php wp_nonce_field( $this->get_feature_key() . '_csrf' ); ?> |
| 186 | <input type="submit" name="wp-submit" id="wp-submit" class="button button-primary button-large" |
| 187 | value="<?php esc_attr_e( 'Log In' ); ?>"/> |
| 188 | <input type="hidden" name="redirect_to" |
| 189 | value="<?php echo esc_attr( sanitize_text_field( $_REQUEST['redirect_to'] ?? admin_url() ) ); ?>"/> |
| 190 | <input type="hidden" name="testcookie" value="1"/> |
| 191 | </p> |
| 192 | </form> |
| 193 | <?php |
| 194 | } |
| 195 | |
| 196 | /** |
| 197 | * デバイス登録がまだのユーザーは、デバイス登録画面にリダイレクト |
| 198 | * |
| 199 | * @param $user_login |
| 200 | * @param $user |
| 201 | * |
| 202 | * @return void |
| 203 | * @noinspection PhpUnusedParameterInspection |
| 204 | */ |
| 205 | public function redirect_if_not_two_factor_authentication_registered( $user_login, $user ) { |
| 206 | $secret = get_user_option( 'cloudsecurewp_two_factor_authentication_secret', $user->ID ); |
| 207 | |
| 208 | if ( isset( $user->roles[0] ) ) { |
| 209 | if ( $this->is_enabled() && $this->is_role_enabled( $user->roles[0] ) && ! $secret && $_SERVER['REQUEST_URI'] !== '/wp-admin/admin.php?page=cloudsecurewp_two_factor_authentication_registration' ) { |
| 210 | wp_redirect( admin_url( 'admin.php?page=cloudsecurewp_two_factor_authentication_registration' ) ); |
| 211 | exit; |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | /** |
| 217 | * WordPress標準機能のユーザー一覧に表示するcolumnを追加 |
| 218 | */ |
| 219 | public function add_2factor_state_2user_list( $columns ) { |
| 220 | $new_columns = []; |
| 221 | |
| 222 | foreach ( $columns as $key => $value ) { |
| 223 | $new_columns[ $key ] = $value; |
| 224 | |
| 225 | if ( $key === 'role' ) { |
| 226 | $new_columns['is_2factor'] = '2段階認証'; |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | return $new_columns; |
| 231 | } |
| 232 | |
| 233 | /** |
| 234 | * WordPress標準機能のユーザー一覧に表示する二段階認証の設定状� |
| 235 | �を指定 |
| 236 | */ |
| 237 | public function show_2factor_state_2user_list( $value, $column_name, $user_id ) { |
| 238 | if ( $column_name === 'is_2factor' ) { |
| 239 | $value = get_user_meta( $user_id, 'wp_cloudsecurewp_two_factor_authentication_secret', true ); |
| 240 | return $value !== '' ? '設定済' : '未設定'; |
| 241 | } |
| 242 | return $value; |
| 243 | } |
| 244 | |
| 245 | /** |
| 246 | * ユーザの2faシークレットキー取得 |
| 247 | * |
| 248 | * @param int $user_id |
| 249 | * |
| 250 | * @return mixed |
| 251 | */ |
| 252 | private function get_2fa_secret_key( int $user_id ) { |
| 253 | return get_user_option( 'cloudsecurewp_two_factor_authentication_secret', $user_id ); |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * option keyを作成 |
| 258 | * |
| 259 | * @param string $token |
| 260 | * |
| 261 | * @return string |
| 262 | */ |
| 263 | private function create_option_key( string $token ): string { |
| 264 | return self::OPTION_PREFIX . $token; |
| 265 | } |
| 266 | |
| 267 | /** |
| 268 | * option dataを登録 |
| 269 | * |
| 270 | * @param string $key |
| 271 | * @param mixed $data |
| 272 | * |
| 273 | * @return void |
| 274 | */ |
| 275 | private function set_option_data( string $key, $data ): void { |
| 276 | update_option( $key, $data, false ); |
| 277 | } |
| 278 | |
| 279 | /** |
| 280 | * option dataを取得 |
| 281 | * |
| 282 | * @param string $key |
| 283 | * |
| 284 | * @return array|false データが存在しないまたは、有効期限切れの場合FALSEを返却 |
| 285 | */ |
| 286 | private function get_option_data( string $key ) { |
| 287 | |
| 288 | $data = get_option( $key ); |
| 289 | |
| 290 | // データが存在しない |
| 291 | if ( ! $data || ! is_array( $data ) ) { |
| 292 | return false; |
| 293 | } |
| 294 | |
| 295 | // 有効期限切れ |
| 296 | if ( ! isset( $data['expires'] ) || $data['expires'] <= time() ) { |
| 297 | return false; |
| 298 | } |
| 299 | |
| 300 | // 有効なデータを返却 |
| 301 | return $data; |
| 302 | } |
| 303 | |
| 304 | /** |
| 305 | * option dataを削除 |
| 306 | * |
| 307 | * @param string $key |
| 308 | * |
| 309 | * @return void |
| 310 | */ |
| 311 | private function delete_option_data( string $key ): void { |
| 312 | delete_option( $key ); |
| 313 | } |
| 314 | |
| 315 | /** |
| 316 | * 2段階認証が� |
| 317 | 要かどうか判定処理 |
| 318 | * |
| 319 | * @param mixed $user |
| 320 | * |
| 321 | * @return bool |
| 322 | */ |
| 323 | private function is_2fa_required( $user ): bool { |
| 324 | |
| 325 | // 2段階認証が無効な場合 |
| 326 | if ( ! $this->is_enabled() ) { |
| 327 | return false; |
| 328 | } |
| 329 | |
| 330 | // 有効な権限グループに含まれない場合 |
| 331 | if ( ! isset( $user->roles[0] ) || ! $this->is_role_enabled( $user->roles[0] ) ) { |
| 332 | return false; |
| 333 | } |
| 334 | |
| 335 | // 2faシークレットキーが存在しない場合 |
| 336 | if ( ! $this->get_2fa_secret_key( $user->ID ) ) { |
| 337 | return false; |
| 338 | } |
| 339 | |
| 340 | return true; |
| 341 | } |
| 342 | |
| 343 | /** |
| 344 | * 2段階認証画面を表示 |
| 345 | * |
| 346 | * @param string $login_token |
| 347 | * |
| 348 | * @return void |
| 349 | */ |
| 350 | private function show_two_factor_form( string $login_token ) { |
| 351 | // 2FA画面を表示 |
| 352 | login_header( '2段階認証画面' ); |
| 353 | $this->login_error(); |
| 354 | $this->login_form( $login_token ); |
| 355 | login_footer(); |
| 356 | exit; |
| 357 | } |
| 358 | |
| 359 | /** |
| 360 | * POSTデータから2FA関連の値を安� |
| 361 | �に取得 |
| 362 | * |
| 363 | * @return array |
| 364 | */ |
| 365 | private function get_2fa_post_data(): array { |
| 366 | return array( |
| 367 | 'login_token' => sanitize_text_field( $_POST['login_token'] ?? '' ), |
| 368 | 'google_authenticator_code' => sanitize_text_field( $_POST['google_authenticator_code'] ?? '' ), |
| 369 | ); |
| 370 | } |
| 371 | |
| 372 | /** |
| 373 | * 2段階認証コード検証処理 |
| 374 | * |
| 375 | * @param int $user_id |
| 376 | * @param string $code |
| 377 | * |
| 378 | * @return bool |
| 379 | */ |
| 380 | private function verify_2fa_code( int $user_id, string $code ): bool { |
| 381 | |
| 382 | // 2faシークレットキー取得 |
| 383 | $secret_key = $this->get_2fa_secret_key( $user_id ); |
| 384 | |
| 385 | // 2faシークレットキーが存在しない場合 |
| 386 | if ( ! $secret_key ) { |
| 387 | return true; |
| 388 | } |
| 389 | |
| 390 | // 2段階認証コードが有効な場合 |
| 391 | if ( CloudSecureWP_Time_Based_One_Time_Password::verify_code( $secret_key, $code, 2 ) ) { |
| 392 | return true; |
| 393 | } |
| 394 | |
| 395 | // 認証失敗 |
| 396 | return false; |
| 397 | } |
| 398 | |
| 399 | /** |
| 400 | * 認証フック: ユーザ名復� |
| 401 | �処理 |
| 402 | * |
| 403 | * @param string $username |
| 404 | * @param bool $strict |
| 405 | * |
| 406 | * @return string |
| 407 | */ |
| 408 | public function restore_login_name( string $username, $strict = false ): string { |
| 409 | |
| 410 | // 初回アクセス・または初回認証の場合、何もしない |
| 411 | if ( ! isset( $_POST['google_authenticator_code'] ) ) { |
| 412 | return $username; |
| 413 | } |
| 414 | |
| 415 | // 2FA関連のPOSTデータ取得 |
| 416 | $post_data = $this->get_2fa_post_data(); |
| 417 | |
| 418 | // ログイン� |
| 419 | 報を取得 |
| 420 | $option_key = $this->create_option_key( $post_data['login_token'] ); |
| 421 | $option_data = $this->get_option_data( $option_key ); |
| 422 | |
| 423 | // ログイン� |
| 424 | 報が存在しない場合、何もしない |
| 425 | if ( $option_data === false ) { |
| 426 | return $username; |
| 427 | } |
| 428 | |
| 429 | // ユーザ名を返却 |
| 430 | return $option_data['user_login']; |
| 431 | } |
| 432 | |
| 433 | /** |
| 434 | * 認証フック: ログインデータ復� |
| 435 | �処理 |
| 436 | * |
| 437 | * @param mixed $user |
| 438 | * @param string $username |
| 439 | * @param string $password |
| 440 | * |
| 441 | * @return mixed |
| 442 | */ |
| 443 | public function restore_login_session( $user, $username, $password ) { |
| 444 | |
| 445 | // 初回アクセス・または初回認証の場合、何もしない |
| 446 | if ( ! isset( $_POST['google_authenticator_code'] ) ) { |
| 447 | return $user; |
| 448 | } |
| 449 | |
| 450 | // CSRFトークンを検証(失敗すると「辿ったリンクは期限が切れています。」のエラー画面を表示し処理終了) |
| 451 | check_admin_referer( $this->get_feature_key() . '_csrf' ); |
| 452 | |
| 453 | // 2FA関連のPOSTデータ取得 |
| 454 | $post_data = $this->get_2fa_post_data(); |
| 455 | |
| 456 | // ログイン� |
| 457 | 報を取得 |
| 458 | $option_key = $this->create_option_key( $post_data['login_token'] ); |
| 459 | $option_data = $this->get_option_data( $option_key ); |
| 460 | |
| 461 | // ログインが有効期限切れの場合 |
| 462 | // ログイン成功時にまとめてクリーンアップ処理を実行するため、ここでは消さない |
| 463 | if ( $option_data === false ) { |
| 464 | return new WP_Error( 'empty_username', 'セッションの有効期限が切れました。再度ログインしてください。' ); |
| 465 | } |
| 466 | |
| 467 | // ユーザーオブジェクト取得 |
| 468 | $user = get_user_by( 'id', $option_data['user_id'] ); |
| 469 | if ( ! $user ) { |
| 470 | $this->delete_option_data( $option_key ); |
| 471 | return new WP_Error( 'empty_username', 'ユーザー� |
| 472 | 報が見つかりません。再度ログインしてください。' ); |
| 473 | } |
| 474 | |
| 475 | // ログイン� |
| 476 | 報のPOSTデータ復� |
| 477 | � |
| 478 | $_POST['log'] = $option_data['user_login']; |
| 479 | |
| 480 | return $user; |
| 481 | } |
| 482 | |
| 483 | /** |
| 484 | * 認証フック: 2段階認証処理 |
| 485 | * |
| 486 | * @param mixed $user |
| 487 | * @param string $username |
| 488 | * @param string $password |
| 489 | * |
| 490 | * @return mixed |
| 491 | */ |
| 492 | public function authenticate_with_two_factor( $user, $username, $password ) { |
| 493 | |
| 494 | // 初回アクセス、または初回認証時 |
| 495 | if ( ! isset( $_POST['google_authenticator_code'] ) ) { |
| 496 | |
| 497 | // 認証失敗の場合 |
| 498 | if ( is_wp_error( $user ) ) { |
| 499 | return $user; |
| 500 | } |
| 501 | |
| 502 | // 2段階認証が不要な場合 |
| 503 | if ( ! $this->is_2fa_required( $user ) ) { |
| 504 | return $user; |
| 505 | } |
| 506 | |
| 507 | // option key生成 |
| 508 | $session_token = bin2hex( random_bytes( 16 ) ); |
| 509 | $option_key = $this->create_option_key( $session_token ); |
| 510 | |
| 511 | // 保存用認証データ作成 |
| 512 | $option_data = array( |
| 513 | 'user_id' => $user->ID, |
| 514 | 'user_login' => sanitize_text_field( $_POST['log'] ?? '' ), |
| 515 | 'expires' => time() + self::SESSION_EXPIRY, |
| 516 | 'created' => time(), |
| 517 | ); |
| 518 | |
| 519 | // データを保存 |
| 520 | $this->set_option_data( $option_key, $option_data ); |
| 521 | |
| 522 | // 2FA画面を表示して、処理終了 |
| 523 | $this->show_two_factor_form( $session_token ); |
| 524 | } |
| 525 | |
| 526 | // 2FA関連のPOSTデータ取得 |
| 527 | $post_data = $this->get_2fa_post_data(); |
| 528 | |
| 529 | // option key取得 |
| 530 | $option_key = $this->create_option_key( $post_data['login_token'] ); |
| 531 | |
| 532 | // $userがWP_Errorの場合は処理をスキップ |
| 533 | if ( is_wp_error( $user ) ) { |
| 534 | return $user; |
| 535 | } |
| 536 | |
| 537 | // 2段階認証成功の場合 |
| 538 | if ( $this->verify_2fa_code( $user->ID, $post_data['google_authenticator_code'] ) ) { |
| 539 | $this->delete_option_data( $option_key ); |
| 540 | return $user; |
| 541 | } |
| 542 | |
| 543 | // ログイン失敗時の処理を実行(ログイン回数・ログインログ) |
| 544 | do_action( 'wp_login_failed', $_POST['log'], $user ); |
| 545 | |
| 546 | // 2FA画面を再表示して、処理終了 |
| 547 | $this->show_two_factor_form( $post_data['login_token'] ); |
| 548 | } |
| 549 | |
| 550 | /** |
| 551 | * 2FAセッションデータを取得 |
| 552 | * |
| 553 | * @param int $last_option_id |
| 554 | * @param int $limit |
| 555 | * |
| 556 | * @return array |
| 557 | */ |
| 558 | private function fetch_2fa_sessions( int $last_option_id, int $limit ): array { |
| 559 | global $wpdb; |
| 560 | |
| 561 | // セッションキーの接頭辞でLIKE検索 |
| 562 | $like = $wpdb->esc_like( self::OPTION_PREFIX ) . '%'; |
| 563 | |
| 564 | // SQL実行(LIKE検索を行うため、意図的に直接クエリを実行する) |
| 565 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 566 | $options = $wpdb->get_results( |
| 567 | $wpdb->prepare( |
| 568 | "SELECT |
| 569 | option_id, |
| 570 | option_name, |
| 571 | option_value |
| 572 | FROM |
| 573 | {$wpdb->options} |
| 574 | WHERE TRUE |
| 575 | AND option_name LIKE %s |
| 576 | AND %d < option_id |
| 577 | ORDER BY |
| 578 | option_id ASC |
| 579 | LIMIT |
| 580 | %d", |
| 581 | $like, |
| 582 | $last_option_id, |
| 583 | $limit |
| 584 | ) |
| 585 | ); |
| 586 | |
| 587 | return $options ?? array(); |
| 588 | } |
| 589 | |
| 590 | /** |
| 591 | * 期限切れセッションデータを収集 |
| 592 | * |
| 593 | * @param array $options |
| 594 | * |
| 595 | * @return array ['log_data' => array, 'delete_option_names' => array] |
| 596 | */ |
| 597 | private function collect_expired_session_data( array $options ): array { |
| 598 | // ログ登録用データリスト初期化 |
| 599 | $log_data = array(); |
| 600 | // 削除対象のoption_nameリスト |
| 601 | $delete_option_names = array(); |
| 602 | |
| 603 | foreach ( $options as $option ) { |
| 604 | // option_valueを連想� |
| 605 | �列に変換 |
| 606 | $data = maybe_unserialize( $option->option_value ); |
| 607 | |
| 608 | // � |
| 609 | �列でない場合はスキップ |
| 610 | if ( ! is_array( $data ) ) { |
| 611 | continue; |
| 612 | } |
| 613 | |
| 614 | // 有効期限が切れている場合 |
| 615 | if ( isset( $data['expires'] ) && $data['expires'] <= time() ) { |
| 616 | |
| 617 | // ログ登録用データを収集 |
| 618 | $log_data[] = array( |
| 619 | 'name' => $data['user_login'], |
| 620 | 'ip' => $this->get_client_ip( '' ), |
| 621 | 'status' => self::LOGIN_STATUS_FAILED, |
| 622 | 'method' => 1, |
| 623 | 'login_at' => wp_date( 'Y-m-d H:i:s', $data['created'] ), // WPのタイムゾーンに変更して登録 |
| 624 | ); |
| 625 | |
| 626 | // 削除対象のoption_nameを収集 |
| 627 | $delete_option_names[] = $option->option_name; |
| 628 | } |
| 629 | } |
| 630 | |
| 631 | return array( |
| 632 | 'log_data' => $log_data, |
| 633 | 'delete_option_names' => $delete_option_names, |
| 634 | ); |
| 635 | } |
| 636 | |
| 637 | /** |
| 638 | * ログイン失敗ログを一括登録 |
| 639 | * (呼び出し� |
| 640 | �でトランザクションを管理すること) |
| 641 | * |
| 642 | * @param array $log_data |
| 643 | * |
| 644 | * @return void |
| 645 | * @throws Exception SQLエラー発生時. |
| 646 | */ |
| 647 | private function insert_login_failed_logs( array $log_data ): void { |
| 648 | if ( empty( $log_data ) ) { |
| 649 | return; |
| 650 | } |
| 651 | |
| 652 | global $wpdb; |
| 653 | |
| 654 | // プレースホルダーと値の準備 |
| 655 | $values = array(); |
| 656 | $placeholders = array(); |
| 657 | |
| 658 | // 収集したログデータを一括登録用に変換 |
| 659 | foreach ( $log_data as $log ) { |
| 660 | $values[] = $log['name']; |
| 661 | $values[] = $log['ip']; |
| 662 | $values[] = $log['status']; |
| 663 | $values[] = $log['method']; |
| 664 | $values[] = $log['login_at']; |
| 665 | $placeholders[] = '(%s, %s, %d, %d, %s)'; |
| 666 | } |
| 667 | |
| 668 | // SQL実行(一括登録を行うため、意図的に直接クエリを実行する) |
| 669 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 670 | $result = $wpdb->query( |
| 671 | $wpdb->prepare( |
| 672 | "INSERT INTO `{$wpdb->prefix}cloudsecurewp_login_log` |
| 673 | (`name`, `ip`, `status`, `method`, `login_at`) |
| 674 | VALUES " . implode( ', ', $placeholders ), |
| 675 | $values |
| 676 | ) |
| 677 | ); |
| 678 | |
| 679 | // SQLエラーチェック |
| 680 | if ( $result === false || ! empty( $wpdb->last_error ) ) { |
| 681 | throw new Exception( 'Failed to insert login logs: ' . $wpdb->last_error ); |
| 682 | } |
| 683 | } |
| 684 | |
| 685 | /** |
| 686 | * 指定されたオプションを一括削除 |
| 687 | * (呼び出し� |
| 688 | �でトランザクションを管理すること) |
| 689 | * |
| 690 | * @param array $option_names |
| 691 | * |
| 692 | * @return void |
| 693 | * @throws Exception SQLエラー発生時. |
| 694 | */ |
| 695 | private function delete_options( array $option_names ): void { |
| 696 | if ( empty( $option_names ) ) { |
| 697 | return; |
| 698 | } |
| 699 | |
| 700 | global $wpdb; |
| 701 | |
| 702 | // プレースホルダー作成 |
| 703 | $placeholders = implode( ', ', array_fill( 0, count( $option_names ), '%s' ) ); |
| 704 | |
| 705 | // SQL実行(一括削除を行うため、意図的に直接クエリを実行する) |
| 706 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 707 | $result = $wpdb->query( |
| 708 | $wpdb->prepare( |
| 709 | "DELETE FROM |
| 710 | {$wpdb->options} |
| 711 | WHERE |
| 712 | option_name IN ($placeholders)", |
| 713 | $option_names |
| 714 | ) |
| 715 | ); |
| 716 | |
| 717 | // SQLエラーチェック |
| 718 | if ( $result === false || ! empty( $wpdb->last_error ) ) { |
| 719 | throw new Exception( 'Failed to delete options: ' . $wpdb->last_error ); |
| 720 | } |
| 721 | } |
| 722 | |
| 723 | /** |
| 724 | * 期限切れログイン� |
| 725 | 報セッションのクリーンアップ処理本体 |
| 726 | * |
| 727 | * @return void |
| 728 | */ |
| 729 | private function process_cleanup_expired_sessions(): void { |
| 730 | global $wpdb; |
| 731 | |
| 732 | $last_option_id = 0; |
| 733 | |
| 734 | while ( true ) { |
| 735 | try { |
| 736 | // 2FAセッションデータを取得 |
| 737 | $options = $this->fetch_2fa_sessions( $last_option_id, self::CLEANUP_BATCH_SIZE ); |
| 738 | |
| 739 | // 取得するレコードがなくなったら終了 |
| 740 | if ( empty( $options ) ) { |
| 741 | break; |
| 742 | } |
| 743 | |
| 744 | // 最後に取得したoption_idを更新 |
| 745 | $last_option_id = end( $options )->option_id; |
| 746 | |
| 747 | // 期限切れセッションデータを収集 |
| 748 | $result = $this->collect_expired_session_data( $options ); |
| 749 | $log_data = $result['log_data']; |
| 750 | $delete_option_names = $result['delete_option_names']; |
| 751 | |
| 752 | if ( empty( $delete_option_names ) && empty( $log_data ) ) { |
| 753 | continue; |
| 754 | } |
| 755 | |
| 756 | // トランザクション開始 |
| 757 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 758 | $wpdb->query( 'START TRANSACTION' ); |
| 759 | |
| 760 | // ログデータを一括登録 |
| 761 | $this->insert_login_failed_logs( $log_data ); |
| 762 | |
| 763 | // オプションを一括削除 |
| 764 | $this->delete_options( $delete_option_names ); |
| 765 | |
| 766 | // トランザクションコミット |
| 767 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 768 | $wpdb->query( 'COMMIT' ); |
| 769 | |
| 770 | } catch ( Exception $e ) { |
| 771 | // エラー発生時はロールバック |
| 772 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 773 | $wpdb->query( 'ROLLBACK' ); |
| 774 | break; |
| 775 | } |
| 776 | } |
| 777 | } |
| 778 | |
| 779 | /** |
| 780 | * 期限切れの2FAセッションをクリーンアップ |
| 781 | * |
| 782 | * @return void |
| 783 | */ |
| 784 | public function cleanup_expired_sessions(): void { |
| 785 | global $wpdb; |
| 786 | |
| 787 | // ロック名 |
| 788 | $lock_name = 'cloudsecurewp_2fa_cleanup_lock'; |
| 789 | // クリーンアップ処理の完了を� |
| 790 | つ最大秒数 |
| 791 | $timeout = self::CLEANUP_TIMEOUT; |
| 792 | |
| 793 | // ロックを取得 |
| 794 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 795 | $get_lock = $wpdb->get_var( |
| 796 | $wpdb->prepare( 'SELECT GET_LOCK(%s, 0)', $lock_name ) |
| 797 | ); |
| 798 | |
| 799 | if ( $get_lock === '1' ) { |
| 800 | // ロック取得成功(クリーンアップ実行� |
| 801 | ) |
| 802 | |
| 803 | try { |
| 804 | // クリーンアップ処理実行 |
| 805 | $this->process_cleanup_expired_sessions(); |
| 806 | } catch ( Exception $e ) { |
| 807 | // クリーンアップ処理実行で失敗しても� |
| 808 | 部でロールバックするため、ここでは何もしない |
| 809 | } finally { |
| 810 | // ロック解放 |
| 811 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 812 | $wpdb->query( |
| 813 | $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) |
| 814 | ); |
| 815 | } |
| 816 | } else { |
| 817 | // ロック取得失敗(� |
| 818 | 機� |
| 819 | ) |
| 820 | |
| 821 | // リーダーのクリーンアップ完了を� |
| 822 | 機 |
| 823 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 824 | $acquired_signal = $wpdb->get_var( |
| 825 | $wpdb->prepare( 'SELECT GET_LOCK(%s, %d)', $lock_name, $timeout ) |
| 826 | ); |
| 827 | |
| 828 | if ( $acquired_signal === '1' ) { |
| 829 | // ロック取得成功(クリーンアップ処理終了) |
| 830 | // 即座にロック解放(� |
| 831 | 機完了の合図として使うだけ) |
| 832 | // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery |
| 833 | $wpdb->query( |
| 834 | $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $lock_name ) |
| 835 | ); |
| 836 | } |
| 837 | } |
| 838 | } |
| 839 | } |
| 840 |