PluginProbe ʕ •ᴥ•ʔ
CloudSecure WP Security / 1.4.8
CloudSecure WP Security v1.4.8
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 2 months 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 3 months 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 2 months ago server-error-table.php 1 year ago two-factor-authentication-registration.php 2 months ago two-factor-authentication.php 2 months ago unify-messages.php 1 year ago update-notice.php 2 months ago waf-table.php 1 year ago waf.php 2 months ago
two-factor-authentication-registration.php
1143 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 int
13 */
14 private $auth_method;
15 /**
16 * リカバリーコードが登録済みかどうか
17 *
18 * @var bool
19 */
20 private $is_registered_recovery;
21 /**
22 * 使用可能なリカバリーコードの数
23 *
24 * @var int
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 integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA=="
351 crossorigin="anonymous"
352 referrerpolicy="no-referrer"></script>
353 <script type="text/javascript">
354 const messageRegist = '認証方法の設定が完了しました。';
355 const messageUpdate = '認証方法の変更が完了しました。';
356 const messageError = 'エラーが発生しました。しばらく�
357 ってから再度お試しください。';
358 const messageAppCodeEmpty = '認証に失敗しました。<br />QRコードをGoogle Authenticator でスキャンし、認証コードを�
359 �力してください。';
360 const messageAppCodeInvalid = '認証に失敗しました。コードが正しいか確認し、もう一度お試しください。';
361 const messageEmailCodeEmpty = '認証に失敗しました。メールで送信された認証コードを�
362 �力してください';
363 const messageEmailCodeInvalid = '認証に失敗しました。認証コードが間違っているか、有効期限が切れています。';
364 const messageEmailSent = '認証コードを再送信しました。メールをご確認ください。';
365 const messageQRCodeError = 'QRコードの生成に失敗しました。セットアップキーを手動で�
366 �力してください。';
367 const ajaxUrl = document.getElementById('ajax-url').value;
368
369 let emailSentTime = null;
370 let countdownTimeout = null;
371 let qrcodeInstance = null;
372
373 // モーダル�
374 のすべてのボタンを非活性処理
375 function disableModalButtons() {
376 const settingModal = document.getElementById('setting-modal');
377 const confirmModal = document.getElementById('confirm-modal');
378
379 if (settingModal && settingModal.style.display === 'block') {
380 const buttons = settingModal.querySelectorAll('button');
381 buttons.forEach(btn => {
382 btn.disabled = true;
383 });
384 // ×ボタンも非活性化
385 const closeIcon = settingModal.querySelector('.dashicons.modal-close');
386 if (closeIcon) {
387 closeIcon.style.pointerEvents = 'none';
388 closeIcon.style.opacity = '0.5';
389 }
390 }
391
392 if (confirmModal && confirmModal.style.display === 'block') {
393 const buttons = confirmModal.querySelectorAll('button');
394 buttons.forEach(btn => {
395 btn.disabled = true;
396 });
397 // ×ボタンも非活性化
398 const closeIcon = confirmModal.querySelector('.dashicons.modal-close');
399 if (closeIcon) {
400 closeIcon.style.pointerEvents = 'none';
401 closeIcon.style.opacity = '0.5';
402 }
403 }
404 }
405 // モーダル�
406 のすべてのボタンを活性処理
407 function enableModalButtons() {
408 const settingModal = document.getElementById('setting-modal');
409 const confirmModal = document.getElementById('confirm-modal');
410
411 if (settingModal && settingModal.style.display === 'block') {
412 const buttons = settingModal.querySelectorAll('button');
413 buttons.forEach(btn => {
414 btn.disabled = false;
415 });
416 // ×ボタンも活性化
417 const closeIcon = settingModal.querySelector('.dashicons.modal-close');
418 if (closeIcon) {
419 closeIcon.style.pointerEvents = 'auto';
420 closeIcon.style.opacity = '1';
421 }
422 }
423
424 if (confirmModal && confirmModal.style.display === 'block') {
425 const buttons = confirmModal.querySelectorAll('button');
426 buttons.forEach(btn => {
427 btn.disabled = false;
428 });
429 // ×ボタンも活性化
430 const closeIcon = confirmModal.querySelector('.dashicons.modal-close');
431 if (closeIcon) {
432 closeIcon.style.pointerEvents = 'auto';
433 closeIcon.style.opacity = '1';
434 }
435 }
436 }
437 // ページリロード処理
438 function reloadPageWithMessage(message, error) {
439 const form = document.createElement('form');
440 form.method = 'POST';
441 form.action = window.location.href;
442
443 const inputMessage = document.createElement('input');
444 inputMessage.type = 'hidden';
445 inputMessage.name = 'message';
446 inputMessage.value = message;
447
448 const inputError = document.createElement('input');
449 inputError.type = 'hidden';
450 inputError.name = 'error';
451 inputError.value = error;
452
453 form.appendChild(inputMessage);
454 form.appendChild(inputError);
455 document.body.appendChild(form);
456 form.submit();
457 }
458 // モーダル�
459 にメッセージ表示処理
460 function showModalMessage(message) {
461 const modalBody = document.getElementById('setting-modal-body');
462 const messageArea = document.getElementById('message-area');
463 const errorArea = document.getElementById('error-area');
464 if (errorArea) {
465 errorArea.innerHTML = '';
466 errorArea.style.display = 'none';
467 }
468 if (messageArea) {
469 messageArea.innerHTML = message;
470 messageArea.style.display = 'block';
471 }
472 setTimeout(() => {
473 modalBody.scrollTop = 0;
474 }, 0);
475 }
476 // モーダル�
477 にエラー表示処理
478 function showModalError(message) {
479 const modalBody = document.getElementById('setting-modal-body');
480 const messageArea = document.getElementById('message-area');
481 const errorArea = document.getElementById('error-area');
482 if (messageArea) {
483 messageArea.innerHTML = '';
484 messageArea.style.display = 'none';
485 }
486 if (errorArea) {
487 errorArea.innerHTML = message;
488 errorArea.style.display = 'block';
489 }
490 setTimeout(() => {
491 modalBody.scrollTop = 0;
492 }, 0);
493 }
494 // 再送信リンクの有効/無効の制御処理
495 function updateResendLinkState() {
496 const resendLink = document.getElementById('resend-email-code');
497 const countdownSpan = document.querySelector('.countdown-message');
498
499 if (!resendLink || !emailSentTime || !countdownSpan) {
500 return;
501 }
502
503 // 現在時刻と送信可能時刻を比較して残り秒数を計算
504 const now = Date.now();
505 const remaining = Math.ceil((emailSentTime - now) / 1000);
506
507 if (remaining > 0) {
508 // リンクを非活性化してカウントダウン表示
509 resendLink.className = 'resend-email-code-inactive';
510 countdownSpan.textContent = ` (${remaining}秒後)`;
511
512 // 1秒後に再度更新
513 countdownTimeout = setTimeout(updateResendLinkState, 1000);
514 } else {
515 // リンクを有効化してカウントダウン非表示
516 resendLink.className = 'resend-email-code-active';
517 countdownSpan.textContent = '';
518
519 // タイマーを停止(これ以上更新不要)
520 countdownTimeout = null;
521 }
522 }
523 // カウントダウンタイマーを開始処理
524 function startCountdown() {
525 // 既存のタイマーがあればクリア
526 if (countdownTimeout) {
527 clearTimeout(countdownTimeout);
528 countdownTimeout = null;
529 }
530
531 // 即座に状�
532 �を更新
533 updateResendLinkState();
534 }
535 // QRコードを生成・表示処理
536 function displayQRCode(key) {
537 const qrcodeElement = document.getElementById('qrcode');
538 const setupKeyDisplay = document.getElementById('setup_key_display');
539
540 if (!qrcodeElement) {
541 console.error('QRコード要素が見つかりません');
542 return;
543 }
544
545 // 既存のQRコードを完�
546 �にクリア
547 qrcodeElement.innerHTML = '';
548 qrcodeInstance = null;
549
550 // 新しいQRCodeインスタンスを作成
551 try {
552 qrcodeInstance = new QRCode(qrcodeElement, {
553 correctLevel: QRCode.CorrectLevel.M,
554 width: 200,
555 height: 200
556 });
557
558 // ユーザー名を取得
559 const nameElement = document.querySelector('.display-name');
560 const name = nameElement ? nameElement.textContent : 'User';
561
562 // QRコードを生成
563 const otpauthUrl = `otpauth://totp/${encodeURIComponent(name)}?secret=${key}&issuer=${location.hostname}`;
564 qrcodeInstance.makeCode(otpauthUrl);
565 } catch (e) {
566 showModalError(messageQRCodeError);
567 }
568
569 // セットアップキーを表示
570 if (setupKeyDisplay) {
571 setupKeyDisplay.textContent = key;
572 }
573
574 // 認証コード�
575 �力欄をクリア&フォーカス
576 setTimeout(function() {
577 const codeInput = document.getElementById('verification-code');
578 if (codeInput) {
579 codeInput.value = '';
580 codeInput.focus();
581 }
582 }, 100);
583 }
584 // メール認証コードを送信処理
585 function generateSecretKeyAndsendEmail(resendFlg) {
586 const nonce = document.getElementById('_wpnonce').value;
587
588 if (!resendFlg) {
589 disableModalButtons();
590 }
591
592 // AJAXでサーバーに送信
593 jQuery.ajax({
594 url: ajaxUrl,
595 type: 'POST',
596 data: {
597 action: 'cloudsecurewp_generate_key_and_send_email',
598 nonce: nonce,
599 }
600 })
601 .done(function(response) {
602 if (response.success) {
603 if (resendFlg === false) {
604 openAuthModal('email');
605 }
606 if (resendFlg && response.data.is_send_email) {
607 showModalMessage(messageEmailSent);
608 }
609 // APIから受け取った残り秒数を使って送信可能時刻を計算
610 const remainingSeconds = parseInt(response.data.remaining_seconds, 10);
611 emailSentTime = Date.now() + (remainingSeconds * 1000);
612 setTimeout(function() {
613 startCountdown();
614 const codeInput = document.getElementById('verification-code');
615 if (codeInput) {
616 codeInput.value = '';
617 codeInput.focus();
618 }
619 if (!resendFlg) {
620 enableModalButtons();
621 }
622 }, 100);
623 } else {
624 reloadPageWithMessage('', messageError);
625 }
626 return;
627 })
628 .fail(function(xhr) {
629 reloadPageWithMessage('', messageError);
630 return;
631 });
632 }
633 // 秘密鍵生成処理(サーバー側から取得)
634 function generateSecretKey(reGenerateFlg) {
635 const nonce = document.getElementById('_wpnonce').value;
636
637 disableModalButtons();
638
639 // AJAXでサーバーから秘密鍵を取得
640 jQuery.ajax({
641 url: ajaxUrl,
642 type: 'POST',
643 data: {
644 action: 'cloudsecurewp_generate_key',
645 nonce: nonce
646 }
647 })
648 .done(function(response) {
649 if (response.success) {
650 const hexKey = response.data.hex;
651 const base32Key = response.data.base32;
652
653 // モーダルを表示
654 if (reGenerateFlg === false) {
655 openAuthModal('app');
656 }
657
658 // DOMが構築されるのを�
659 ってから処理を実行
660 setTimeout(function() {
661 const secretKeyEle = document.getElementById('secret-key');
662 if (secretKeyEle) {
663 secretKeyEle.value = hexKey;
664 }
665 displayQRCode(base32Key);
666 enableModalButtons();
667 }, 100);
668 } else {
669 reloadPageWithMessage('', messageError);
670 }
671 return;
672 })
673 .fail(function(xhr) {
674 reloadPageWithMessage('', messageError);
675 return;
676 });
677 }
678 // 認証コード検証処理
679 function verifyCode(method) {
680 const secretKey = document.getElementById('secret-key').value;
681 const codeInput = document.getElementById('verification-code');
682 const code = codeInput.value.trim();
683 const nextBtn = document.getElementById('setting-modal-next');
684 const authMethodStatus = document.getElementById('auth-method-status').dataset.authMethod;
685 const isRegistered = (authMethodStatus !== '0') ? true : false;
686 const nonce = document.getElementById('_wpnonce').value;
687
688 disableModalButtons();
689
690 if (!code) {
691 if (method === 'app') {
692 showModalError(messageAppCodeEmpty);
693 } else {
694 showModalError(messageEmailCodeEmpty);
695 }
696 enableModalButtons();
697 codeInput.value = '';
698 codeInput.focus();
699 } else {
700 // AJAXでサーバーに送信
701 jQuery.ajax({
702 url: ajaxUrl,
703 type: 'POST',
704 data: {
705 action: 'cloudsecurewp_verify_auth_code',
706 nonce: nonce,
707 secret_key: secretKey,
708 code: code,
709 method: method
710 }
711 })
712 .done(function(response) {
713 if (response.success) {
714 if (response.data.has_recovery) {
715 closeSettingModal();
716 if (isRegistered) {
717 reloadPageWithMessage(messageUpdate, '');
718 } else {
719 reloadPageWithMessage(messageRegist, '');
720 }
721 } else {
722 document.getElementById('setting-modal').dataset.registStatus = isRegistered ? 'update' : 'regist';
723 openSuggestRecoveryModal();
724 enableModalButtons();
725 }
726 return;
727 } else {
728 if (method === 'app') {
729 showModalError(messageAppCodeInvalid);
730 } else {
731 showModalError(messageEmailCodeInvalid);
732 }
733 enableModalButtons();
734 codeInput.value = '';
735 codeInput.focus();
736 return;
737 }
738 })
739 .fail(function(xhr) {
740 reloadPageWithMessage('', messageError);
741 return;
742 })
743 }
744 }
745 // リカバリーコード生成処理
746 function generateRecoveryCode(openModalFlg) {
747 const nonce = document.getElementById('_wpnonce').value;
748
749 if (openModalFlg) {
750 disableModalButtons();
751 }
752
753 // AJAXでサーバーに送信
754 jQuery.ajax({
755 url: ajaxUrl,
756 type: 'POST',
757 data: {
758 action: 'cloudsecurewp_generate_recovery_codes',
759 nonce: nonce
760 }
761 })
762 .done(function(response) {
763 if (response.success && Array.isArray(response.data.codes) && response.data.codes.length > 0) {
764 // 成功時のリカバリーコードモーダルを表示
765 closeConfirmModal();
766 openRecoveryCodesSuccessModal(response.data.codes);
767 enableModalButtons();
768 return;
769 } else {
770 // 失敗時のリカバリーコードモーダルを表示
771 closeConfirmModal();
772 openRecoveryCodesFailureModal();
773 enableModalButtons();
774 return;
775 }
776 })
777 .fail(function(xhr) {
778 // 失敗時のリカバリーコードモーダルを表示
779 closeConfirmModal();
780 openRecoveryCodesFailureModal();
781 enableModalButtons();
782 return;
783 })
784 }
785 // リカバリーコードのコピー処理
786 function codeCopy() {
787 const codeItems = document.querySelectorAll('.recovery-code-item');
788 const codes = Array.from(codeItems).map(item => item.textContent);
789 const codesText = codes.join('\n');
790
791 // HTTPS環境またはlocalhost: Clipboard APIを使用
792 if (navigator.clipboard && navigator.clipboard.writeText) {
793 navigator.clipboard.writeText(codesText).then(function() {
794 alert('クリップボードにコピーしました。');
795 }).catch(function() {
796 fallbackCopy(codesText);
797 });
798 } else {
799 // HTTP環境: フォールバック処理
800 fallbackCopy(codesText);
801 }
802 }
803 // HTTP環境用のフォールバックコピー処理
804 function fallbackCopy(text) {
805 const textarea = document.createElement('textarea');
806 textarea.value = text;
807 textarea.style.position = 'fixed';
808 textarea.style.top = '0';
809 textarea.style.left = '0';
810 textarea.style.opacity = '0';
811 document.body.appendChild(textarea);
812 textarea.focus();
813 textarea.select();
814
815 try {
816 const successful = document.execCommand('copy');
817 if (successful) {
818 alert('クリップボードにコピーしました。');
819 } else {
820 alert('コピーに失敗しました。手動でコードをコピーしてください。');
821 }
822 } catch (err) {
823 alert('コピーに失敗しました。手動でコードをコピーしてください。');
824 } finally {
825 document.body.removeChild(textarea);
826 }
827 }
828 // リカバリーコードのダウンロード処理
829 function codeDownload() {
830 const codeItems = document.querySelectorAll('.recovery-code-item');
831 const codes = Array.from(codeItems).map(item => item.textContent);
832 const codesText = codes.join('\n');
833 const blob = new Blob([codesText], { type: 'text/plain' });
834 const url = URL.createObjectURL(blob);
835 const a = document.createElement('a');
836 a.href = url;
837 a.download = 'recovery-codes.txt';
838 a.style.display = 'none';
839 document.body.appendChild(a);
840 a.click();
841
842 setTimeout(function() {
843 document.body.removeChild(a);
844 URL.revokeObjectURL(url);
845 }, 100);
846 }
847 // 選択モーダル表示処理
848 function openSelectModal() {
849 const settingModal = document.getElementById('setting-modal');
850 const targetTitle = document.getElementById('setting-modal-title');
851 const targetBody = document.getElementById('setting-modal-body');
852 const template = document.getElementById('select-method-modal');
853
854 if (template) {
855 settingModal.dataset.settingStatus = 'select_method';
856
857 targetTitle.textContent = '認証方法の設定';
858 targetBody.innerHTML = '';
859
860 const clone = document.importNode(template.content, true);
861 targetBody.appendChild(clone);
862
863 settingModal.style.display = 'block';
864 document.body.style.overflowY = 'hidden';
865 }
866 }
867 // 認証モーダル表示処理
868 function openAuthModal(method) {
869 const settingModal = document.getElementById('setting-modal');
870 const settingTitle = document.getElementById('setting-modal-title');
871 const settingBody = document.getElementById('setting-modal-body');
872 let template = null;
873
874 settingModal.dataset.settingStatus = method;
875
876 if (method === 'app') {
877 template = document.getElementById('app-auth-modal');
878 } else if (method === 'email') {
879 template = document.getElementById('email-auth-modal');
880 }
881
882 settingBody.innerHTML = '';
883
884 const clone = document.importNode(template.content, true);
885 settingBody.appendChild(clone);
886 }
887 // リカバリーコード生成提案モーダル表示処理
888 function openSuggestRecoveryModal() {
889 const settingModal = document.getElementById('setting-modal');
890 const targetTitle = document.getElementById('setting-modal-title');
891 const targetBody = document.getElementById('setting-modal-body');
892 const template = document.getElementById('suggest-recovery-modal');
893 const clone = document.importNode(template.content, true);
894 settingModal.dataset.settingStatus = 'suggest_recovery';
895 targetBody.innerHTML = '';
896 targetBody.appendChild(clone);
897 }
898 // 成功時リカバリーコードモーダル表示処理
899 function openRecoveryCodesSuccessModal(codes) {
900 const settingModal = document.getElementById('setting-modal');
901 const settingTitle = document.getElementById('setting-modal-title');
902 const settingBody = document.getElementById('setting-modal-body');
903
904 settingModal.dataset.settingStatus = 'recovery_codes';
905 settingTitle.textContent = 'リカバリーコード';
906 settingBody.innerHTML = '';
907
908 const template = document.getElementById('recovery-code-modal-success');
909 const clone = document.importNode(template.content, true);
910 settingBody.appendChild(clone);
911
912 // リカバリーコードを表示
913 const container = document.getElementById('recovery-codes-container');
914 codes.forEach(function(code) {
915 const codeItem = document.createElement('div');
916 codeItem.className = 'recovery-code-item';
917 codeItem.textContent = code;
918 container.appendChild(codeItem);
919 });
920
921 settingBody.className = 'recovery-modal-body';
922 settingModal.style.display = 'block';
923 }
924 // 失敗時リカバリーコードモーダル表示処理
925 function openRecoveryCodesFailureModal() {
926 const settingModal = document.getElementById('setting-modal');
927 const settingTitle = document.getElementById('setting-modal-title');
928 const settingBody = document.getElementById('setting-modal-body');
929
930 settingModal.dataset.settingStatus = 'recovery_codes';
931 settingTitle.textContent = 'リカバリーコードの生成に失敗しました';
932 settingBody.innerHTML = '';
933
934 const template = document.getElementById('recovery-code-modal-failure');
935 const clone = document.importNode(template.content, true);
936 settingBody.appendChild(clone);
937
938 settingBody.className = 'recovery-modal-body';
939 settingModal.style.display = 'block';
940 }
941 // 確認モーダル表示処理
942 function openConfirmModal(status) {
943 const contentBody = document.getElementById('content-body');
944 const modal = document.getElementById('confirm-modal');
945 const title = document.getElementById('confirm-message-title');
946 const modalBody = document.getElementById('confirm-message-body');
947
948 if (status === 'recovery') {
949 title.textContent = 'リカバリーコードを再生成しますか?';
950 modalBody.innerHTML = `
951 <div class="recovery-text">
952 <div class="black-circle"></div><p class="modal-text">誰にも見られていない安�
953 �な環境で実行してください。</p>
954 </div>
955 <div class="recovery-text">
956 <div class="black-circle"></div><p class="modal-text">再生成を行うと既存のリカバリーコードはすべて使用できなくなります。</p>
957 </div>
958 `;
959 document.body.style.overflowY = 'hidden';
960 } else {
961 title.textContent = '処理を中断しますか?';
962 modalBody.innerHTML = '<p class="modal-text">保存していない変更は失われます。</p>';
963 }
964
965 modal.dataset.confirmStatus = status;
966 modal.style.display = 'block';
967 }
968 // 設定モーダル非表示処理
969 function closeSettingModal() {
970 const settingModal = document.getElementById('setting-modal');
971 const settingTitle = document.getElementById('setting-modal-title');
972 const settingBody = document.getElementById('setting-modal-body');
973
974 settingTitle.innerHTML = '';
975 settingBody.innerHTML = '';
976 settingBody.className = 'setting-modal-body';
977 settingModal.style.display = 'none';
978 }
979 // 確認モーダル非表示処理
980 function closeConfirmModal() {
981 const confirmModal = document.getElementById('confirm-modal');
982 const confirmTitle = document.getElementById('confirm-message-title');
983 const confirmBody = document.getElementById('confirm-message-body');
984
985 confirmTitle.innerHTML = '';
986 confirmBody.innerHTML = '';
987 confirmModal.style.display = 'none';
988 }
989 // 設定モーダルの次へボタン押下時のアクション
990 function handleSettingNext(status) {
991 if (status === 'select_method') {
992 // 次へボタン押下時の処理
993 const selectedMethod = document.querySelector('input[name="auth_method_select"]:checked').value;
994 if (selectedMethod === 'app') {
995 // アプリ認証の場合はQRコード生成処理を実行
996 generateSecretKey(false);
997 } else {
998 generateSecretKeyAndsendEmail(false);
999 }
1000 } else if (status === 'app' || status === 'email') {
1001 // 設定を完了ボタン押下時の処理
1002 verifyCode(status);
1003 } else if (status === 'suggest_recovery' || status === 'recovery_codes') {
1004 // リカバリーコードを生成ボタン押下時の処理
1005 generateRecoveryCode(true);
1006 }
1007 }
1008 // 確認モーダルの次へボタン押下時のアクション
1009 function handleConfirmNext(status) {
1010 if (status === 'recovery') {
1011 generateRecoveryCode(true);
1012 } else {
1013 if (countdownTimeout) {
1014 clearTimeout(countdownTimeout);
1015 countdownTimeout = null;
1016 }
1017 closeConfirmModal();
1018 closeSettingModal();
1019 document.body.style.overflowY = 'auto';
1020 }
1021 }
1022 // 設定モーダルの閉じるボタン押下時のアクション
1023 function handleSettingClose(status) {
1024 if (status === 'select_method') {
1025 closeSettingModal();
1026 document.body.style.overflowY = 'auto';
1027 } else if (status === 'suggest_recovery' || status === 'recovery_codes') {
1028 const registStatus = document.getElementById('setting-modal').dataset.registStatus;
1029 if (registStatus === 'regist') {
1030 reloadPageWithMessage(messageRegist, '');
1031 } else if (registStatus === 'update') {
1032 reloadPageWithMessage(messageUpdate, '');
1033 } else {
1034 reloadPageWithMessage('', '');
1035 }
1036 } else {
1037 openConfirmModal(status);
1038 }
1039 }
1040 // リカバリーコード生成ボタン押下時のアクション
1041 function handleCreateRecovery() {
1042 // リカバリーコードの生成状�
1043 �を確認
1044 const registerMethodBtn = document.getElementById('register-method');
1045 const generateRecoveryBtn = document.getElementById('generate-recovery');
1046 const isRegisteredRecovery = document.getElementById('recovery-code-status').dataset.isRegisteredRecovery;
1047 if (isRegisteredRecovery === '1') {
1048 // 既にリカバリーコードが登録されている場合は確認モーダルを表示
1049 openConfirmModal('recovery');
1050 } else {
1051 // リカバリーコードが未登録の場合は直接生成処理を実行
1052 registerMethodBtn.disabled = true;
1053 generateRecoveryBtn.disabled = true;
1054 generateRecoveryCode(false);
1055 }
1056 }
1057
1058 // 画面読み込み時の処理
1059 document.addEventListener('DOMContentLoaded', function() {
1060 const registerMethodBtn = document.getElementById('register-method');
1061 const generateRecoveryBtn = document.getElementById('generate-recovery');
1062 const settingModal = document.getElementById('setting-modal');
1063 const confirmModal = document.getElementById('confirm-modal');
1064
1065 // 認証方法「設定」ボタンのイベント付与
1066 if (registerMethodBtn) {
1067 registerMethodBtn.addEventListener('click', function() {
1068 openSelectModal();
1069 })
1070 }
1071 // リカバリーコード「生成」ボタンのイベント付与
1072 if (generateRecoveryBtn) {
1073 generateRecoveryBtn.addEventListener('click', function() {
1074 handleCreateRecovery();
1075 })
1076 }
1077 // 設定モーダル�
1078 のボタンイベント付与
1079 if (settingModal) {
1080 settingModal.addEventListener('click', (e) => {
1081 const nextSettingBtn = e.target.closest('.modal-next');
1082 const closeSettingBtn = e.target.closest('.modal-close');
1083 const regenerateKeyBtn = e.target.closest('#regenerate-key');
1084 const resendEmailBtn = e.target.closest('#resend-email-code');
1085 const codeCopyBtn = e.target.closest('#recovery-code-copy');
1086 const codeDownloadBtn = e.target.closest('#recovery-code-download');
1087 // �
1088 �通「次へ」ボタンイベント付与
1089 if (nextSettingBtn) {
1090 const settingStatus = settingModal.dataset.settingStatus;
1091 handleSettingNext(settingStatus);
1092 }
1093 // �
1094 �通「キャンセル」ボタン「×」ボタンイベント付与
1095 if (closeSettingBtn) {
1096 const settingStatus = settingModal.dataset.settingStatus;
1097 handleSettingClose(settingStatus);
1098 }
1099 // シークレットキー再生成リンクイベント付与
1100 if (regenerateKeyBtn) {
1101 generateSecretKey(true);
1102 }
1103 // メール認証コード再送信リンクイベント付与
1104 if (resendEmailBtn) {
1105 generateSecretKeyAndsendEmail(true);
1106 }
1107 // リカバリーコードコピーイベント付与
1108 if (codeCopyBtn) {
1109 codeCopy();
1110 }
1111 // リカバリーコードダウンロードイベント付与
1112 if (codeDownloadBtn) {
1113 codeDownload();
1114 }
1115 });
1116 }
1117 // 確認モーダル�
1118 のボタンイベント付与
1119 if (confirmModal) {
1120 confirmModal.addEventListener('click', (e) => {
1121 const nextConfirmBtn = e.target.closest('.modal-next');
1122 const closeConfirmBtn = e.target.closest('.modal-close');
1123
1124 // �
1125 �通「次へ」ボタンイベント付与
1126 if (nextConfirmBtn) {
1127 const confirmStatus = confirmModal.dataset.confirmStatus;
1128 handleConfirmNext(confirmStatus);
1129 }
1130 // �
1131 �通「キャンセル」ボタン「×」ボタンイベント付与
1132 if (closeConfirmBtn) {
1133 closeConfirmModal();
1134 }
1135 })
1136 }
1137 })
1138 </script>
1139
1140 <?php
1141 }
1142 }
1143