PluginProbe ʕ •ᴥ•ʔ
CloudSecure WP Security / 1.4.3
CloudSecure WP Security v1.4.3
1.4.10 1.4.9 trunk 0.9.0 1.0.2 1.1.0 1.1.1 1.1.2 1.1.3 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.3.1 1.3.10 1.3.11 1.3.12 1.3.13 1.3.14 1.3.15 1.3.16 1.3.17 1.3.18 1.3.19 1.3.2 1.3.20 1.3.21 1.3.22 1.3.23 1.3.24 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8
cloudsecure-wp-security / modules / admin / two-factor-authentication-registration.php
cloudsecure-wp-security / modules / admin Last commit date
captcha.php 1 year ago common.php 1 year ago dashboard.php 1 year ago disable-access-system-file.php 7 months ago disable-author-query.php 1 year ago disable-login.php 1 year ago disable-restapi.php 1 year ago disable-xmlrpc.php 1 year ago login-log-table.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 1 year ago server-error-notification.php 1 year ago server-error-table.php 1 year ago two-factor-authentication-registration.php 4 months ago two-factor-authentication.php 4 months ago unify-messages.php 1 year ago update-notice.php 1 year ago waf-table.php 1 year ago waf.php 7 months ago
two-factor-authentication-registration.php
1135 lines
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 class CloudSecureWP_Admin_Two_Factor_Authentication_Registration extends CloudSecureWP_Admin_Common {
8 private $two_factor_authentication;
9 /**
10 * 認証方法
11 *
12 * @var bool
13 */
14 private $auth_method;
15 /**
16 * リカバリーコードが登録済みかどうか
17 *
18 * @var bool
19 */
20 private $is_registered_recovery;
21 /**
22 * 使用可能なリカバリーコードの数
23 *
24 * @var bool
25 */
26 private $recovery_cnt;
27 /**
28 * ユーザのメールアドレス
29 *
30 * @var string
31 */
32 private $mail_address;
33
34 function __construct( array $info, CloudSecureWP_Two_Factor_Authentication $two_factor_authentication ) {
35 parent::__construct( $info );
36 $this->two_factor_authentication = $two_factor_authentication;
37 $this->prepare_view_data();
38 $this->render();
39 }
40
41 /**
42 * 画面表示用のデータを準備
43 */
44 public function prepare_view_data(): void {
45 $user_id = get_current_user_id();
46 $auth_info = $this->two_factor_authentication->get_2fa_auth_info( $user_id );
47
48 $this->auth_method = $this->two_factor_authentication::USER_AUTH_METHOD_NONE;
49 $this->is_registered_recovery = false;
50 $this->recovery_cnt = 0;
51 $this->mail_address = $this->two_factor_authentication->mask_email( $user_id );
52
53 if ( count( $auth_info ) === 0 ) {
54 $auth_info = $this->two_factor_authentication->repair_migration_gaps( $user_id );
55 }
56
57 if ( count( $auth_info ) > 0 ) {
58 $this->auth_method = intVal( $auth_info['method'] );
59
60 if ( ! is_null( $auth_info['recovery'] ) ) {
61 $this->is_registered_recovery = true;
62 $this->recovery_cnt = count( $auth_info['recovery'] );
63 }
64 }
65
66 if ( isset( $_POST['message'] ) ) {
67 $message = sanitize_text_field( $_POST['message'] ) ?? '';
68 if ( ! empty( $message ) ) {
69 $this->messages[] = $message;
70 }
71 }
72 if ( isset( $_POST['error'] ) ) {
73 $error = sanitize_text_field( $_POST['error'] ) ?? '';
74 if ( ! empty( $error ) ) {
75 $this->errors[] = $error;
76 }
77 }
78 }
79
80 /**
81 * ディスクリプション
82 */
83 protected function admin_description(): void {
84 ?>
85 <div class="title-block mb-12">
86 <h1 class="title-block-title">2段階認証の設定</h1>
87 <p class="title-block-small-text">この機能のマニュアルは<a class="title-block-link" target="_blank"
88 href="https://wpplugin.cloudsecure.ne.jp/cloudsecure_wp_security/two_factor_authentication.php">こちら</a>
89 </p>
90 </div>
91 <div class="title-bottom-text">
92 2段階認証に使用する認証方法を設定します。<br />
93 Google Authenticator またはメール認証のいずれかを使用できます。
94 </div>
95 <?php
96 }
97
98 /**
99 * ページコンテンツ
100 */
101 protected function page(): void {
102 ?>
103 <div id="two-fa-setting-area">
104 <div class="box">
105 <div class="box-bottom">
106 <div class="box-row">
107 <div class="box-row-title not-label">
108 <label for="key">設定方法</label>
109 </div>
110 <div class="box-row-content">
111 <div id="auth-method-status" class="status-area" data-auth-method="<?php echo esc_attr( $this->auth_method ); ?>">
112 <?php if ( $this->auth_method === $this->two_factor_authentication::USER_AUTH_METHOD_APP ) : ?>
113 <div class="status-area-text">
114 <p class="status-text">Authenticatorアプリ</p>
115 </div>
116 <?php elseif ( $this->auth_method === $this->two_factor_authentication::USER_AUTH_METHOD_EMAIL ) : ?>
117 <div class="status-area-text">
118 <p class="status-text">メール認証</p>
119 </div>
120 <?php else : ?>
121 <div class="status-not-registered-back-none status-area-text">
122 <span class="dashicons dashicons-warning"></span>
123 <p class="status-text">未設定</p>
124 </div>
125 <?php endif; ?>
126 <button id="register-method" type="button" class="status-area-btn button">
127 <?php echo ( $this->auth_method === $this->two_factor_authentication::USER_AUTH_METHOD_NONE ? '設定' : '設定を変更' ); ?>
128 </button>
129 </div>
130 </div>
131 </div>
132 <div class="box-row">
133 <div class="box-row-title not-label">
134 <label for="key">リカバリーコード</label>
135 <div class="description-recovery-code">
136 <p class="description-text">デバイス紛失時などの緊急時、<br class="pc-only">認証代わりに使用できるコードです。</p>
137 </div>
138 </div>
139 <div class="box-row-content">
140 <div id="recovery-code-status" class="status-area" data-is-registered-recovery="<?php echo esc_attr( $this->is_registered_recovery ? 1 : 0 ); ?>">
141 <div class="status-area-text">
142 <?php if ( $this->is_registered_recovery ) : ?>
143 <p class="status-text">生成済み(残り<?php echo esc_html( $this->recovery_cnt ); ?>個)</p>
144 <?php else : ?>
145 <div class="status-not-registered-back-none status-area-text">
146 <span class="dashicons dashicons-warning"></span>
147 <p class="status-text">未生成</p>
148 </div>
149 <?php if ( $this->auth_method === $this->two_factor_authentication::USER_AUTH_METHOD_NONE ) : ?>
150 <p class="status-text description">※認証方法の設定完了後、生成可能になります。</p>
151 <?php endif; ?>
152 <?php endif; ?>
153 </div>
154 <button id="generate-recovery" type="button" class="status-area-btn button" <?php echo ( $this->auth_method === $this->two_factor_authentication::USER_AUTH_METHOD_NONE ? 'disabled' : '' ); ?>>
155 <?php echo ( $this->is_registered_recovery ? '再生成' : '生成' ); ?>
156 </button>
157 </div>
158 </div>
159 </div>
160 </div>
161 </div>
162
163 <!-- 設定モーダル -->
164 <div id="setting-modal" class="setting-modal" style="display: none;" data-setting-status="" data-regist-status="">
165 <div class="setting-modal-content">
166 <div class="setting-modal-header">
167 <h2 id="setting-modal-title">
168 <!-- タイトル -->
169 </h2>
170 <span class="dashicons dashicons-no-alt modal-close"></span>
171 </div>
172 <div id="setting-modal-body" class="setting-modal-body">
173 <!-- テンプレート -->
174 </div>
175 </div>
176 </div>
177
178 <!-- 確認モーダル -->
179 <div id="confirm-modal" class="confirm-modal" style="display: none;" data-confirm-status="">
180 <div class="confirm-modal-content">
181 <div class="confirm-modal-body">
182 <div class="confirm-message-area">
183 <h2 id="confirm-message-title">
184 <!-- タイトル -->
185 </h2>
186 <div id="confirm-message-body" class="confirm-message-body">
187 <!-- 本文 -->
188 </div>
189 </div>
190 <div id="confirm-modal-btn" class="modal-btn-area-end">
191 <button type="button" id="confirm-modal-cancel" class="button button-gray modal-close style-cancel">キャンセル</button>
192 <button type="button" id="confirm-modal-ok" class="button button-blue modal-next style-next">OK</button>
193 </div>
194 </div>
195 </div>
196 </div>
197
198 <?php $this->nonce_wp( $this->two_factor_authentication->get_feature_key() ); ?>
199 <input type="hidden" id="ajax-url" value="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" />
200 </div>
201
202 <!-- 認証方法選択テンプレート -->
203 <template id="select-method-modal">
204 <p class="modal-text">認証方法を選択してください。</p>
205 <div class="auth-method-options">
206 <div class="auth-method-option">
207 <input type="radio" class="circle-radio" id="auth_method_app" name="auth_method_select" value="app" checked>
208 <label for="auth_method_app" class="auth-method-label">
209 <img src="<?php echo esc_url( plugin_dir_url( dirname( __FILE__, 2 ) ) . 'assets/images/icon_mobile.svg?v=1' ); ?>" alt="スマホアイコン" class="auth-method-icon">
210 <span class="auth-method-text">Google Authenticatorで認証する</span>
211 </label>
212 </div>
213 <div class="auth-method-option">
214 <input type="radio" class="circle-radio" id="auth_method_email" name="auth_method_select" value="email">
215 <label for="auth_method_email" class="auth-method-label">
216 <img src="<?php echo esc_url( plugin_dir_url( dirname( __FILE__, 2 ) ) . 'assets/images/icon_mail.svg?v=1' ); ?>" alt="メールアイコン" class="auth-method-icon">
217 <span class="auth-method-text">メールアドレスで認証する</span>
218 </label>
219 </div>
220 </div>
221 <div class="modal-btn-area-end">
222 <button type="button" id="setting-modal-cancel" class="button button-gray modal-close">キャンセル</button>
223 <button type="button" id="setting-modal-next" class="button button-blue modal-next">次へ</button>
224 </div>
225 </template>
226
227 <!-- Google Authenticator設定テンプレート -->
228 <template id="app-auth-modal">
229 <div id="message-area" class="info-box blue" style="display: none;"></div>
230 <div id="error-area" class="error-box red" style="display: none;"></div>
231 <p class="modal-text">
232 <a class="title-block-link" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2" target="_blank">Google Authenticator</a> で以下のQRコードを読み込み、表示された認証コードを�
233 �力してください。QRコードを読み込めない場合は、セットアップキーを手動で�
234 �力してください。
235 </p>
236 <div class="qr-setup-container">
237 <div id="qrcode_container" class="qr-code-section">
238 <div id="qrcode">
239 <!-- QRコード表示エリア -->
240 </div>
241 </div>
242 <div class="setup-key-section">
243 <div class="generate-key-section" style="text-align: left;">
244 <p class="modal-text">セットアップキー:<span id="setup_key_display"></span></p>
245 <p class="modal-text" style="color: #2271b1;"><span class="dashicons dashicons-update"></span><a href="#" id="regenerate-key">新しいキーを生成</a></p>
246 </div>
247 <div class="input-code-section">
248 <label for="verification-code">認証コード(6桁)</label>
249 <input type="text" id="verification-code" name="verification_code" class="verification-code-app" placeholder="例)123456" />
250 </div>
251 </div>
252 </div>
253 <div class="modal-btn-area-end">
254 <input type="hidden" id="secret-key" value="" />
255 <button type="button" id="setting-modal-cancel" class="button button-gray modal-close">キャンセル</button>
256 <button type="button" id="setting-modal-next" class="button button-blue modal-next">設定を完了</button>
257 </div>
258 </template>
259
260 <!-- メール認証設定テンプレート -->
261 <template id="email-auth-modal">
262 <div id="message-area" class="info-box blue" style="display: none;"></div>
263 <div id="error-area" class="error-box red" style="display: none;"></div>
264 <p class="modal-text">
265 ログイン時に登録されているメールアドレスに認証コードを送信しました。<br />
266 受信した認証コードを�
267 �力してください。<br />
268 メールアドレス: <strong><?php echo esc_html( $this->mail_address ); ?></strong>
269 </p>
270 <div class="input-code-section">
271 <label for="verification-code">認証コード(6桁)</label>
272 <input type="text" id="verification-code" name="verification_code" class="verification-code-email" placeholder="例)123456" />
273 </div>
274 <p class="description">
275 ※メールが届かない場合は、迷惑メールフォルダをご確認ください。<br />
276  メールが見つからない場合は、<a href="#" id="resend-email-code" class="resend-email-code-inactive">認証コードを再送信</a> できます。<span class="countdown-message"></span>
277 </p>
278 <div class="modal-btn-area-end">
279 <input type="hidden" id="secret-key" value="" />
280 <button type="button" id="setting-modal-cancel" class="button button-gray modal-close">キャンセル</button>
281 <button type="button" id="setting-modal-next" class="button button-blue modal-next">設定を完了</button>
282 </div>
283 </template>
284
285 <!-- リカバリーコードの作成案�
286 テンプレート -->
287 <template id="suggest-recovery-modal">
288 <div class="status-registered-back-none">
289 <span class="dashicons dashicons-yes-alt"></span>
290 <p class="h2-text"><strong>2段階認証が有効になりました。</strong></p>
291 </div>
292 <div class="recovery-text-area">
293 <p class="modal-text">続けて、<strong>リカバリーコードを生成し、安�
294 �な場所に保管してください。</strong></p>
295 <p class="modal-text">未生成の場合、デバイス紛失時などにログインできなくなる恐れがあります。</p>
296 </div>
297 <div class="modal-btn-area-end">
298 <button type="button" id="setting-modal-cancel" class="button button-gray modal-close">あとで生成</button>
299 <button type="button" id="setting-modal-next" class="button button-blue modal-next">リカバリーコードを生成</button>
300 </div>
301 </template>
302
303 <!-- リカバリーコードテンプレート(成功) -->
304 <template id="recovery-code-modal-success">
305 <div class="recovery-modal-body-top">
306 <div class="recovery-text-area">
307 <p class="modal-text" style="margin-bottom: 4px;">リカバリーコードを生成しました。</p>
308 <div class="recovery-text">
309 <div class="black-circle"></div><p class="modal-text">この画面を閉じると<strong>コードは再表示できません</strong></p>
310 </div>
311 <div class="recovery-text">
312 <div class="black-circle"></div><p class="modal-text">各コードは1回のみ使用できます。</p>
313 </div>
314 <div class="recovery-text">
315 <div class="black-circle"></div><p class="modal-text">�
316 ずコピーまたはダウンロードして安�
317 �な場所に保管してください。</p>
318 </div>
319 </div>
320 <div id="recovery-codes-container" class="recovery-codes-grid">
321 <!-- リカバリーコード表示エリア -->
322 </div>
323 <div class="modal-btn-area-center">
324 <button type="button" id="recovery-code-copy" class="button button-blue">コピー</button>
325 <button type="button" id="recovery-code-download" class="button button-blue">ダウンロード</button>
326 </div>
327 </div>
328 <div class="recovery-modal-body-bottom">
329 <div class="modal-btn-area-end">
330 <button type="button" id="recovery-modal-close" class="button modal-close">閉じる</button>
331 </div>
332 </div>
333 </template>
334
335 <!-- リカバリーコードテンプレート(失敗) -->
336 <template id="recovery-code-modal-failure">
337 <div class="recovery-modal-body-top">
338 <p class="modal-text">エラーが発生しました。しばらく�
339 ってから再度お試しください。</p>
340 </div>
341 <div class="recovery-modal-body-bottom">
342 <div class="modal-btn-area-end">
343 <button type="button" id="recovery-modal-close" class="button modal-close">閉じる</button>
344 <button type="button" id="recovery-modal-retry" class="button modal-next">再試行</button>
345 </div>
346 </div>
347 </template>
348
349 <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"
350 type="text/javascript"></script>
351 <script type="text/javascript">
352 const messageRegist = '認証方法の設定が完了しました。';
353 const messageUpdate = '認証方法の変更が完了しました。';
354 const messageError = 'エラーが発生しました。しばらく�
355 ってから再度お試しください。';
356 const messageAppCodeEmpty = '認証に失敗しました。<br />QRコードをGoogle Authenticator でスキャンし、認証コードを�
357 �力してください。';
358 const messageAppCodeInvalid = '認証に失敗しました。コードが正しいか確認し、もう一度お試しください。';
359 const messageEmailCodeEmpty = '認証に失敗しました。メールで送信された認証コードを�
360 �力してください';
361 const messageEmailCodeInvalid = '認証に失敗しました。認証コードが間違っているか、有効期限が切れています。';
362 const messageEmailSent = '認証コードを再送信しました。メールをご確認ください。';
363 const ajaxUrl = document.getElementById('ajax-url').value;
364
365 let emailSentTime = null;
366 let countdownTimeout = null;
367 let qrcodeInstance = null;
368
369 // モーダル�
370 のすべてのボタンを非活性処理
371 function disableModalButtons() {
372 const settingModal = document.getElementById('setting-modal');
373 const confirmModal = document.getElementById('confirm-modal');
374
375 if (settingModal && settingModal.style.display === 'block') {
376 const buttons = settingModal.querySelectorAll('button');
377 buttons.forEach(btn => {
378 btn.disabled = true;
379 });
380 // ×ボタンも非活性化
381 const closeIcon = settingModal.querySelector('.dashicons.modal-close');
382 if (closeIcon) {
383 closeIcon.style.pointerEvents = 'none';
384 closeIcon.style.opacity = '0.5';
385 }
386 }
387
388 if (confirmModal && confirmModal.style.display === 'block') {
389 const buttons = confirmModal.querySelectorAll('button');
390 buttons.forEach(btn => {
391 btn.disabled = true;
392 });
393 // ×ボタンも非活性化
394 const closeIcon = confirmModal.querySelector('.dashicons.modal-close');
395 if (closeIcon) {
396 closeIcon.style.pointerEvents = 'none';
397 closeIcon.style.opacity = '0.5';
398 }
399 }
400 }
401 // モーダル�
402 のすべてのボタンを活性処理
403 function enableModalButtons() {
404 const settingModal = document.getElementById('setting-modal');
405 const confirmModal = document.getElementById('confirm-modal');
406
407 if (settingModal && settingModal.style.display === 'block') {
408 const buttons = settingModal.querySelectorAll('button');
409 buttons.forEach(btn => {
410 btn.disabled = false;
411 });
412 // ×ボタンも活性化
413 const closeIcon = settingModal.querySelector('.dashicons.modal-close');
414 if (closeIcon) {
415 closeIcon.style.pointerEvents = 'auto';
416 closeIcon.style.opacity = '1';
417 }
418 }
419
420 if (confirmModal && confirmModal.style.display === 'block') {
421 const buttons = confirmModal.querySelectorAll('button');
422 buttons.forEach(btn => {
423 btn.disabled = false;
424 });
425 // ×ボタンも活性化
426 const closeIcon = confirmModal.querySelector('.dashicons.modal-close');
427 if (closeIcon) {
428 closeIcon.style.pointerEvents = 'auto';
429 closeIcon.style.opacity = '1';
430 }
431 }
432 }
433 // ページリロード処理
434 function reloadPageWithMessage(message, error) {
435 const form = document.createElement('form');
436 form.method = 'POST';
437 form.action = window.location.href;
438
439 const inputMessage = document.createElement('input');
440 inputMessage.type = 'hidden';
441 inputMessage.name = 'message';
442 inputMessage.value = message;
443
444 const inputError = document.createElement('input');
445 inputError.type = 'hidden';
446 inputError.name = 'error';
447 inputError.value = error;
448
449 form.appendChild(inputMessage);
450 form.appendChild(inputError);
451 document.body.appendChild(form);
452 form.submit();
453 }
454 // モーダル�
455 にメッセージ表示処理
456 function showModalMessage(message) {
457 const modalBody = document.getElementById('setting-modal-body');
458 const messageArea = document.getElementById('message-area');
459 const errorArea = document.getElementById('error-area');
460 if (errorArea) {
461 errorArea.innerHTML = '';
462 errorArea.style.display = 'none';
463 }
464 if (messageArea) {
465 messageArea.innerHTML = message;
466 messageArea.style.display = 'block';
467 }
468 setTimeout(() => {
469 modalBody.scrollTop = 0;
470 }, 0);
471 }
472 // モーダル�
473 にエラー表示処理
474 function showModalError(message) {
475 const modalBody = document.getElementById('setting-modal-body');
476 const messageArea = document.getElementById('message-area');
477 const errorArea = document.getElementById('error-area');
478 if (messageArea) {
479 messageArea.innerHTML = '';
480 messageArea.style.display = 'none';
481 }
482 if (errorArea) {
483 errorArea.innerHTML = message;
484 errorArea.style.display = 'block';
485 }
486 setTimeout(() => {
487 modalBody.scrollTop = 0;
488 }, 0);
489 }
490 // 再送信リンクの有効/無効の制御処理
491 function updateResendLinkState() {
492 const resendLink = document.getElementById('resend-email-code');
493 const countdownSpan = document.querySelector('.countdown-message');
494
495 if (!resendLink || !emailSentTime || !countdownSpan) {
496 return;
497 }
498
499 // 現在時刻と送信可能時刻を比較して残り秒数を計算
500 const now = Date.now();
501 const remaining = Math.ceil((emailSentTime - now) / 1000);
502
503 if (remaining > 0) {
504 // リンクを非活性化してカウントダウン表示
505 resendLink.className = 'resend-email-code-inactive';
506 countdownSpan.textContent = ` (${remaining}秒後)`;
507
508 // 1秒後に再度更新
509 countdownTimeout = setTimeout(updateResendLinkState, 1000);
510 } else {
511 // リンクを有効化してカウントダウン非表示
512 resendLink.className = 'resend-email-code-active';
513 countdownSpan.textContent = '';
514
515 // タイマーを停止(これ以上更新不要)
516 countdownTimeout = null;
517 }
518 }
519 // カウントダウンタイマーを開始処理
520 function startCountdown() {
521 // 既存のタイマーがあればクリア
522 if (countdownTimeout) {
523 clearTimeout(countdownTimeout);
524 countdownTimeout = null;
525 }
526
527 // 即座に状�
528 �を更新
529 updateResendLinkState();
530 }
531 // QRコードを生成・表示処理
532 function displayQRCode(key) {
533 const qrcodeElement = document.getElementById('qrcode');
534 const setupKeyDisplay = document.getElementById('setup_key_display');
535
536 if (!qrcodeElement) {
537 console.error('QRコード要素が見つかりません');
538 return
539 }
540
541 // 既存のQRコードを完�
542 �にクリア
543 qrcodeElement.innerHTML = '';
544 qrcodeInstance = null;
545
546 // 新しいQRCodeインスタンスを作成
547 qrcodeInstance = new QRCode(qrcodeElement, {
548 correctLevel: QRCode.CorrectLevel.M,
549 width: 200,
550 height: 200
551 });
552
553 // ユーザー名を取得
554 const nameElement = document.querySelector('.display-name');
555 const name = nameElement ? nameElement.textContent : 'User';
556
557 // QRコードを生成
558 const otpauthUrl = `otpauth://totp/${encodeURIComponent(name)}?secret=${key}&issuer=${location.hostname}`;
559 qrcodeInstance.makeCode(otpauthUrl);
560
561 // セットアップキーを表示
562 if (setupKeyDisplay) {
563 setupKeyDisplay.textContent = key;
564 }
565
566 // 認証コード�
567 �力欄をクリア&フォーカス
568 setTimeout(function() {
569 const codeInput = document.getElementById('verification-code');
570 if (codeInput) {
571 codeInput.value = '';
572 codeInput.focus();
573 }
574 }, 100);
575 }
576 // メール認証コードを送信処理
577 function generateSecretKeyAndsendEmail(resendFlg) {
578 const nonce = document.getElementById('_wpnonce').value;
579
580 if (!resendFlg) {
581 disableModalButtons();
582 }
583
584 // AJAXでサーバーに送信
585 jQuery.ajax({
586 url: ajaxUrl,
587 type: 'POST',
588 data: {
589 action: 'cloudsecurewp_generate_key_and_send_email',
590 nonce: nonce,
591 }
592 })
593 .done(function(response) {
594 if (response.success) {
595 if (resendFlg === false) {
596 openAuthModal('email');
597 }
598 if (resendFlg && response.data.is_send_email) {
599 showModalMessage(messageEmailSent);
600 }
601 // APIから受け取った残り秒数を使って送信可能時刻を計算
602 const remainingSeconds = parseInt(response.data.remaining_seconds, 10);
603 emailSentTime = Date.now() + (remainingSeconds * 1000);
604 setTimeout(function() {
605 startCountdown();
606 const codeInput = document.getElementById('verification-code');
607 if (codeInput) {
608 codeInput.value = '';
609 codeInput.focus();
610 }
611 if (!resendFlg) {
612 enableModalButtons();
613 }
614 }, 100);
615 } else {
616 reloadPageWithMessage('', messageError);
617 }
618 return;
619 })
620 .fail(function(xhr) {
621 reloadPageWithMessage('', messageError);
622 return;
623 });
624 }
625 // 秘密鍵生成処理(サーバー側から取得)
626 function generateSecretKey(reGenerateFlg) {
627 const nonce = document.getElementById('_wpnonce').value;
628
629 disableModalButtons();
630
631 // AJAXでサーバーから秘密鍵を取得
632 jQuery.ajax({
633 url: ajaxUrl,
634 type: 'POST',
635 data: {
636 action: 'cloudsecurewp_generate_key',
637 nonce: nonce
638 }
639 })
640 .done(function(response) {
641 if (response.success) {
642 const hexKey = response.data.hex;
643 const base32Key = response.data.base32;
644
645 // モーダルを表示
646 if (reGenerateFlg === false) {
647 openAuthModal('app');
648 }
649
650 // DOMが構築されるのを�
651 ってから処理を実行
652 setTimeout(function() {
653 const secretKeyEle = document.getElementById('secret-key');
654 if (secretKeyEle) {
655 secretKeyEle.value = hexKey;
656 }
657 displayQRCode(base32Key);
658 enableModalButtons();
659 }, 100);
660 } else {
661 reloadPageWithMessage('', messageError);
662 }
663 return;
664 })
665 .fail(function(xhr) {
666 reloadPageWithMessage('', messageError);
667 return;
668 });
669 }
670 // 認証コード検証処理
671 function verifyCode(method) {
672 const secretKey = document.getElementById('secret-key').value;
673 const codeInput = document.getElementById('verification-code');
674 const code = codeInput.value.trim();
675 const nextBtn = document.getElementById('setting-modal-next');
676 const authMethodStatus = document.getElementById('auth-method-status').dataset.authMethod;
677 const isRegistered = (authMethodStatus !== '0') ? true : false;
678 const nonce = document.getElementById('_wpnonce').value;
679
680 disableModalButtons();
681
682 if (!code) {
683 if (method === 'app') {
684 showModalError(messageAppCodeEmpty);
685 } else {
686 showModalError(messageEmailCodeEmpty);
687 }
688 enableModalButtons();
689 codeInput.value = '';
690 codeInput.focus();
691 } else {
692 // AJAXでサーバーに送信
693 jQuery.ajax({
694 url: ajaxUrl,
695 type: 'POST',
696 data: {
697 action: 'cloudsecurewp_verify_auth_code',
698 nonce: nonce,
699 secret_key: secretKey,
700 code: code,
701 method: method
702 }
703 })
704 .done(function(response) {
705 if (response.success) {
706 if (response.data.has_recovery) {
707 closeSettingModal();
708 if (isRegistered) {
709 reloadPageWithMessage(messageUpdate, '');
710 } else {
711 reloadPageWithMessage(messageRegist, '');
712 }
713 } else {
714 document.getElementById('setting-modal').dataset.registStatus = isRegistered ? 'update' : 'regist';
715 openSuggestRecoveryModal();
716 enableModalButtons();
717 }
718 return;
719 } else {
720 if (method === 'app') {
721 showModalError(messageAppCodeInvalid);
722 } else {
723 showModalError(messageEmailCodeInvalid);
724 }
725 enableModalButtons();
726 codeInput.value = '';
727 codeInput.focus();
728 return;
729 }
730 })
731 .fail(function(xhr) {
732 reloadPageWithMessage('', messageError);
733 return;
734 })
735 }
736 }
737 // リカバリーコード生成処理
738 function generateRecoveryCode(openModalFlg) {
739 const nonce = document.getElementById('_wpnonce').value;
740
741 if (openModalFlg) {
742 disableModalButtons();
743 }
744
745 // AJAXでサーバーに送信
746 jQuery.ajax({
747 url: ajaxUrl,
748 type: 'POST',
749 data: {
750 action: 'cloudsecurewp_generate_recovery_codes',
751 nonce: nonce
752 }
753 })
754 .done(function(response) {
755 if (response.success && Array.isArray(response.data.codes) && response.data.codes.length > 0) {
756 // 成功時のリカバリーコードモーダルを表示
757 closeConfirmModal();
758 openRecoveryCodesSuccessModal(response.data.codes)
759 enableModalButtons();
760 return;
761 } else {
762 // 失敗時のリカバリーコードモーダルを表示
763 closeConfirmModal();
764 openRecoveryCodesFailureModal()
765 enableModalButtons();
766 return;
767 }
768 })
769 .fail(function(xhr) {
770 // 失敗時のリカバリーコードモーダルを表示
771 closeConfirmModal();
772 openRecoveryCodesFailureModal()
773 enableModalButtons();
774 return;
775 })
776 }
777 // リカバリーコードのコピー処理
778 function codeCopy() {
779 const codeItems = document.querySelectorAll('.recovery-code-item');
780 const codes = Array.from(codeItems).map(item => item.textContent);
781 const codesText = codes.join('\n');
782
783 // HTTPS環境またはlocalhost: Clipboard APIを使用
784 if (navigator.clipboard && navigator.clipboard.writeText) {
785 navigator.clipboard.writeText(codesText).then(function() {
786 alert('クリップボードにコピーしました。');
787 }).catch(function() {
788 fallbackCopy(codesText);
789 });
790 } else {
791 // HTTP環境: フォールバック処理
792 fallbackCopy(codesText);
793 }
794 }
795 // HTTP環境用のフォールバックコピー処理
796 function fallbackCopy(text) {
797 const textarea = document.createElement('textarea');
798 textarea.value = text;
799 textarea.style.position = 'fixed';
800 textarea.style.top = '0';
801 textarea.style.left = '0';
802 textarea.style.opacity = '0';
803 document.body.appendChild(textarea);
804 textarea.focus();
805 textarea.select();
806
807 try {
808 const successful = document.execCommand('copy');
809 if (successful) {
810 alert('クリップボードにコピーしました。');
811 } else {
812 alert('コピーに失敗しました。手動でコードをコピーしてください。');
813 }
814 } catch (err) {
815 alert('コピーに失敗しました。手動でコードをコピーしてください。');
816 } finally {
817 document.body.removeChild(textarea);
818 }
819 }
820 // リカバリーコードのダウンロード処理
821 function codeDownload() {
822 const codeItems = document.querySelectorAll('.recovery-code-item');
823 const codes = Array.from(codeItems).map(item => item.textContent);
824 const codesText = codes.join('\n');
825 const blob = new Blob([codesText], { type: 'text/plain' });
826 const url = URL.createObjectURL(blob);
827 const a = document.createElement('a');
828 a.href = url;
829 a.download = 'recovery-codes.txt';
830 a.style.display = 'none';
831 document.body.appendChild(a);
832 a.click();
833
834 setTimeout(function() {
835 document.body.removeChild(a);
836 URL.revokeObjectURL(url);
837 }, 100);
838 }
839 // 選択モーダル表示処理
840 function openSelectModal() {
841 const settingModal = document.getElementById('setting-modal');
842 const targetTitle = document.getElementById('setting-modal-title');
843 const targetBody = document.getElementById('setting-modal-body');
844 const template = document.getElementById('select-method-modal');
845
846 if (template) {
847 settingModal.dataset.settingStatus = 'select_method';
848
849 targetTitle.textContent = '認証方法の設定';
850 targetBody.innerHTML = '';
851
852 const clone = document.importNode(template.content, true);
853 targetBody.appendChild(clone);
854
855 settingModal.style.display = 'block';
856 document.body.style.overflowY = 'hidden';
857 }
858 }
859 // 認証モーダル表示処理
860 function openAuthModal(method) {
861 const settingModal = document.getElementById('setting-modal');
862 const settingTitle = document.getElementById('setting-modal-title');
863 const settingBody = document.getElementById('setting-modal-body');
864 let template = null;
865
866 settingModal.dataset.settingStatus = method;
867
868 if (method === 'app') {
869 template = document.getElementById('app-auth-modal');
870 } else if (method === 'email') {
871 template = document.getElementById('email-auth-modal');
872 }
873
874 settingBody.innerHTML = '';
875
876 const clone = document.importNode(template.content, true);
877 settingBody.appendChild(clone);
878 }
879 // リカバリーコード生成提案モーダル表示処理
880 function openSuggestRecoveryModal() {
881 const settingModal = document.getElementById('setting-modal');
882 const targetTitle = document.getElementById('setting-modal-title');
883 const targetBody = document.getElementById('setting-modal-body');
884 const template = document.getElementById('suggest-recovery-modal');
885 const clone = document.importNode(template.content, true);
886 settingModal.dataset.settingStatus = 'suggest_recovery';
887 targetBody.innerHTML = '';
888 targetBody.appendChild(clone);
889 }
890 // 成功時リカバリーコードモーダル表示処理
891 function openRecoveryCodesSuccessModal(codes) {
892 const settingModal = document.getElementById('setting-modal');
893 const settingTitle = document.getElementById('setting-modal-title');
894 const settingBody = document.getElementById('setting-modal-body');
895
896 settingModal.dataset.settingStatus = 'recovery_codes';
897 settingTitle.textContent = 'リカバリーコード';
898 settingBody.innerHTML = '';
899
900 const template = document.getElementById('recovery-code-modal-success');
901 const clone = document.importNode(template.content, true);
902 settingBody.appendChild(clone);
903
904 // リカバリーコードを表示
905 const container = document.getElementById('recovery-codes-container');
906 codes.forEach(function(code) {
907 const codeItem = document.createElement('div');
908 codeItem.className = 'recovery-code-item';
909 codeItem.textContent = code;
910 container.appendChild(codeItem);
911 })
912
913 settingBody.className = 'recovery-modal-body';
914 settingModal.style.display = 'block';
915 }
916 // 失敗時リカバリーコードモーダル表示処理
917 function openRecoveryCodesFailureModal() {
918 const settingModal = document.getElementById('setting-modal');
919 const settingTitle = document.getElementById('setting-modal-title');
920 const settingBody = document.getElementById('setting-modal-body');
921
922 settingModal.dataset.settingStatus = 'recovery_codes';
923 settingTitle.textContent = 'リカバリーコードの生成に失敗しました';
924 settingBody.innerHTML = '';
925
926 const template = document.getElementById('recovery-code-modal-failure');
927 const clone = document.importNode(template.content, true);
928 settingBody.appendChild(clone);
929
930 settingBody.className = 'recovery-modal-body';
931 settingModal.style.display = 'block';
932 }
933 // 確認モーダル表示処理
934 function openConfirmModal(status) {
935 const contentBody = document.getElementById('content-body');
936 const modal = document.getElementById('confirm-modal');
937 const title = document.getElementById('confirm-message-title');
938 const modalBody = document.getElementById('confirm-message-body');
939
940 if (status === 'recovery') {
941 title.textContent = 'リカバリーコードを再生成しますか?';
942 modalBody.innerHTML = `
943 <div class="recovery-text">
944 <div class="black-circle"></div><p class="modal-text">誰にも見られていない安�
945 �な環境で実行してください。</p>
946 </div>
947 <div class="recovery-text">
948 <div class="black-circle"></div><p class="modal-text">再生成を行うと既存のリカバリーコードはすべて使用できなくなります。</p>
949 </div>
950 `;
951 document.body.style.overflowY = 'hidden';
952 } else {
953 title.textContent = '処理を中断しますか?';
954 modalBody.innerHTML = '<p class="modal-text">保存していない変更は失われます。</p>';
955 }
956
957 modal.dataset.confirmStatus = status;
958 modal.style.display = 'block';
959 }
960 // 設定モーダル非表示処理
961 function closeSettingModal() {
962 const settingModal = document.getElementById('setting-modal');
963 const settingTitle = document.getElementById('setting-modal-title');
964 const settingBody = document.getElementById('setting-modal-body');
965
966 settingTitle.innerHTML = '';
967 settingBody.innerHTML = '';
968 settingBody.className = 'setting-modal-body';
969 settingModal.style.display = 'none';
970 }
971 // 確認モーダル非表示処理
972 function closeConfirmModal() {
973 const confirmModal = document.getElementById('confirm-modal');
974 const confirmTitle = document.getElementById('confirm-message-title');
975 const confirmBody = document.getElementById('confirm-message-body');
976
977 confirmTitle.innerHTML = '';
978 confirmBody.innerHTML = '';
979 confirmModal.style.display = 'none';
980 }
981 // 設定モーダルの次へボタン押下時のアクション
982 function handleSettingNext(status) {
983 if (status === 'select_method') {
984 // 次へボタン押下時の処理
985 const selectedMethod = document.querySelector('input[name="auth_method_select"]:checked').value;
986 if (selectedMethod === 'app') {
987 // アプリ認証の場合はQRコード生成処理を実行
988 generateSecretKey(false);
989 } else {
990 generateSecretKeyAndsendEmail(false);
991 }
992 } else if (status === 'app' || status === 'email') {
993 // 設定を完了ボタン押下時の処理
994 verifyCode(status);
995 } else if (status === 'suggest_recovery' || status === 'recovery_codes') {
996 // リカバリーコードを生成ボタン押下時の処理
997 generateRecoveryCode(true);
998 }
999 }
1000 // 確認モーダルの次へボタン押下時のアクション
1001 function handleConfirmNext(status) {
1002 if (status === 'recovery') {
1003 generateRecoveryCode(true);
1004 } else {
1005 if (countdownTimeout) {
1006 clearTimeout(countdownTimeout);
1007 countdownTimeout = null;
1008 }
1009 closeConfirmModal();
1010 closeSettingModal();
1011 document.body.style.overflowY = 'auto';
1012 }
1013 }
1014 // 設定モーダルの閉じるボタン押下時のアクション
1015 function handleSettingClose(status) {
1016 if (status === 'select_method') {
1017 closeSettingModal();
1018 document.body.style.overflowY = 'auto';
1019 } else if (status === 'suggest_recovery' || status === 'recovery_codes') {
1020 const registStatus = document.getElementById('setting-modal').dataset.registStatus;
1021 if (registStatus === 'regist') {
1022 reloadPageWithMessage(messageRegist, '');
1023 } else if (registStatus === 'update') {
1024 reloadPageWithMessage(messageUpdate, '');
1025 } else {
1026 reloadPageWithMessage('', '');
1027 }
1028 } else {
1029 openConfirmModal(status);
1030 }
1031 }
1032 // リカバリーコード生成ボタン押下時のアクション
1033 function handleCreateRecovery() {
1034 // リカバリーコードの生成状�
1035 �を確認
1036 const registerMethodBtn = document.getElementById('register-method');
1037 const generateRecoveryBtn = document.getElementById('generate-recovery');
1038 const isRegisteredRecovery = document.getElementById('recovery-code-status').dataset.isRegisteredRecovery;
1039 if (isRegisteredRecovery === '1') {
1040 // 既にリカバリーコードが登録されている場合は確認モーダルを表示
1041 openConfirmModal('recovery');
1042 } else {
1043 // リカバリーコードが未登録の場合は直接生成処理を実行
1044 registerMethodBtn.disabled = true;
1045 generateRecoveryBtn.disabled = true;
1046 generateRecoveryCode(false);
1047 }
1048 }
1049
1050 // 画面読み込み時の処理
1051 document.addEventListener('DOMContentLoaded', function() {
1052 const registerMethodBtn = document.getElementById('register-method');
1053 const generateRecoveryBtn = document.getElementById('generate-recovery');
1054 const settingModal = document.getElementById('setting-modal');
1055 const confirmModal = document.getElementById('confirm-modal');
1056
1057 // 認証方法「設定」ボタンのイベント付与
1058 if (registerMethodBtn) {
1059 registerMethodBtn.addEventListener('click', function() {
1060 openSelectModal();
1061 })
1062 }
1063 // リカバリーコード「生成」ボタンのイベント付与
1064 if (generateRecoveryBtn) {
1065 generateRecoveryBtn.addEventListener('click', function() {
1066 handleCreateRecovery();
1067 })
1068 }
1069 // 設定モーダル�
1070 のボタンイベント付与
1071 if (settingModal) {
1072 settingModal.addEventListener('click', (e) => {
1073 const nextSettingBtn = e.target.closest('.modal-next');
1074 const closeSettingBtn = e.target.closest('.modal-close');
1075 const regenerateKeyBtn = e.target.closest('#regenerate-key');
1076 const resendEmailBtn = e.target.closest('#resend-email-code');
1077 const codeCopyBtn = e.target.closest('#recovery-code-copy');
1078 const codeDownloadBtn = e.target.closest('#recovery-code-download');
1079 // �
1080 �通「次へ」ボタンイベント付与
1081 if (nextSettingBtn) {
1082 const settingStatus = settingModal.dataset.settingStatus;
1083 handleSettingNext(settingStatus);
1084 }
1085 // �
1086 �通「キャンセル」ボタン「×」ボタンイベント付与
1087 if (closeSettingBtn) {
1088 const settingStatus = settingModal.dataset.settingStatus;
1089 handleSettingClose(settingStatus);
1090 }
1091 // シークレットキー再生成リンクイベント付与
1092 if (regenerateKeyBtn) {
1093 generateSecretKey(true);
1094 }
1095 // メール認証コード再送信リンクイベント付与
1096 if (resendEmailBtn) {
1097 generateSecretKeyAndsendEmail(true);
1098 }
1099 // リカバリーコードコピーイベント付与
1100 if (codeCopyBtn) {
1101 codeCopy();
1102 }
1103 // リカバリーコードダウンロードイベント付与
1104 if (codeDownloadBtn) {
1105 codeDownload();
1106 }
1107 });
1108 }
1109 // 確認モーダル�
1110 のボタンイベント付与
1111 if (confirmModal) {
1112 confirmModal.addEventListener('click', (e) => {
1113 const nextConfirmBtn = e.target.closest('.modal-next');
1114 const closeConfirmBtn = e.target.closest('.modal-close');
1115
1116 // �
1117 �通「次へ」ボタンイベント付与
1118 if (nextConfirmBtn) {
1119 const confirmStatus = confirmModal.dataset.confirmStatus;
1120 handleConfirmNext(confirmStatus);
1121 }
1122 // �
1123 �通「キャンセル」ボタン「×」ボタンイベント付与
1124 if (closeConfirmBtn) {
1125 closeConfirmModal();
1126 }
1127 })
1128 }
1129 })
1130 </script>
1131
1132 <?php
1133 }
1134 }
1135