PluginProbe
404 Solution / 4.1.13
404 Solution v4.1.13
4.3.5 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 4.2.0 4.1.19 4.1.18 4.1.17 4.1.16 4.1.15 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.1.7 4.1.6 4.1.5 4.1.4 4.1.3 trunk 2.30.0 All 109 releases
404-solution / includes / SetupWizard.php

SetupWizard.php in 404 Solution 4.1.13, at includes/SetupWizard.php

880 lines 33.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3
4 if (!defined('ABSPATH')) {
5 exit;
6 }
7
8 /**
9 * Setup Wizard for first-time plugin configuration
10 * Shows a welcome modal on first visit to 404 Solution admin pages
11 *
12 * @since 3.0.5
13 */
14 class ABJ_404_Solution_SetupWizard {
15
16 /**
17 * Option name for storing setup completion date
18 */
19 const OPTION_NAME = 'abj404_setup_completed';
20
21 /**
22 * Initialize the setup wizard functionality
23 * @return void
24 */
25 public static function init(): void {
26 // Handle form submission immediately (must run before any output)
27 // This is called early during plugin load, so we check and handle here
28 if (is_admin() && isset($_POST['abj404_setup_wizard_action'])) {
29 // Use admin_init to ensure WordPress is fully loaded for nonce verification
30 add_action('admin_init', array(__CLASS__, 'handleFormSubmission'), 1);
31 }
32
33 // AJAX handler for skip/close (no page reload needed)
34 add_action('wp_ajax_abj404_dismiss_setup_wizard', array(__CLASS__, 'handleAjaxDismiss'));
35
36 // Enqueue assets and output modal on 404 Solution pages
37 add_action('admin_enqueue_scripts', array(__CLASS__, 'enqueueAssets'));
38 }
39
40 /**
41 * Handle AJAX dismiss (skip/close) - no settings changed, just mark complete
42 * @return void
43 */
44 public static function handleAjaxDismiss(): void {
45 // Verify nonce
46 if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'abj404_setup_wizard')) {
47 wp_send_json_error(array('message' => __('Invalid security token', '404-solution')), 403);
48 }
49
50 // Verify user capabilities
51 if (!current_user_can('manage_options')) {
52 wp_send_json_error(array('message' => __('Insufficient permissions', '404-solution')), 403);
53 }
54
55 // Mark setup as complete
56 update_option(self::OPTION_NAME, gmdate('Y-m-d'));
57
58 // Response is intentionally minimal; the UI uses a fire-and-forget request.
59 wp_send_json_success(array('message' => ''));
60 }
61
62 /**
63 * Check if setup wizard should be shown
64 *
65 * @return bool True if wizard should display
66 */
67 private static function shouldShowWizard() {
68 // Only show if setup hasn't been completed
69 // Existing users upgrading from <3.0.7 have this set via migration in PluginLogic.php
70 $completed = get_option(self::OPTION_NAME, '');
71 return empty($completed);
72 }
73
74 /**
75 * Check if current page is a 404 Solution admin page
76 *
77 * @return bool True if on 404 Solution page
78 */
79 private static function isPluginPage() {
80 if (!is_admin()) {
81 return false;
82 }
83
84 // Check for the plugin's page parameter
85 $page = isset($_GET['page']) ? sanitize_text_field($_GET['page']) : '';
86 return $page === 'abj404_solution';
87 }
88
89 /**
90 * Handle form submission for setup wizard
91 * @return void
92 */
93 public static function handleFormSubmission(): void {
94 // Check if this is our form submission
95 if (!isset($_POST['abj404_setup_wizard_action'])) {
96 return;
97 }
98
99 // Verify nonce with error feedback (Bug #10 fix)
100 if (!isset($_POST['abj404_setup_wizard_nonce']) ||
101 !wp_verify_nonce($_POST['abj404_setup_wizard_nonce'], 'abj404_setup_wizard')) {
102 wp_die(
103 esc_html__('Security check failed. Please try again.', '404-solution'),
104 esc_html__('Error', '404-solution'),
105 array('response' => 403, 'back_link' => true)
106 );
107 }
108
109 // Verify user capabilities with error feedback (Bug #10 fix)
110 if (!current_user_can('manage_options')) {
111 wp_die(
112 esc_html__('You do not have permission to access this page.', '404-solution'),
113 esc_html__('Error', '404-solution'),
114 array('response' => 403, 'back_link' => true)
115 );
116 }
117
118 $action = sanitize_text_field($_POST['abj404_setup_wizard_action']);
119
120 // All actions mark setup as complete
121 update_option(self::OPTION_NAME, gmdate('Y-m-d'));
122
123 // If user clicked "Save & Get Started", apply their settings
124 if ($action === 'save') {
125 self::applySettings();
126 }
127
128 // Determine redirect destination based on logging choice
129 $q2_answer = isset($_POST['abj404_setup_q2']) ? sanitize_text_field($_POST['abj404_setup_q2']) : 'yes';
130 $redirect_url = 'options-general.php?page=abj404_solution&setup_complete=1';
131
132 // If logging 404s, take them to Captured 404s tab; otherwise Page Redirects
133 if ($q2_answer === 'yes') {
134 $redirect_url .= '&subpage=abj404_captured';
135 }
136
137 wp_safe_redirect(admin_url($redirect_url));
138 exit;
139 }
140
141 /** Allowed values for Q1 (Bug #13 fix)
142 * @var array<int, string>
143 */
144 private static $allowedQ1Values = ['redirect', 'default'];
145
146 /** Allowed values for Q2 (Bug #13 fix)
147 * @var array<int, string>
148 */
149 private static $allowedQ2Values = ['yes', 'no'];
150
151 /** Allowed values for Q3
152 * @var array<int, string>
153 */
154 private static $allowedQ3Values = ['yes', 'no'];
155
156 /**
157 * Apply settings from wizard form
158 * @return void
159 */
160 private static function applySettings(): void {
161 $abj404logic = abj_service('plugin_logic');
162 $options = $abj404logic->getOptions();
163
164 // Question 1: What happens when page not found
165 // Validate against whitelist (Bug #13 fix)
166 $q1_answer = isset($_POST['abj404_setup_q1']) ? sanitize_text_field($_POST['abj404_setup_q1']) : 'redirect';
167 if (!in_array($q1_answer, self::$allowedQ1Values, true)) {
168 $q1_answer = 'redirect'; // Default to safe value
169 }
170
171 if ($q1_answer === 'redirect') {
172 // Automatically redirect to similar page when a match is found
173 $options['auto_redirects'] = '1';
174 $options['auto_cats'] = '1';
175 $options['auto_tags'] = '1';
176 } else {
177 // Just show the default 404 page - only use manual redirects
178 $options['auto_redirects'] = '0';
179 $options['auto_cats'] = '0';
180 $options['auto_tags'] = '0';
181 }
182 $options['dest404page'] = '0|' . ABJ404_TYPE_404_DISPLAYED;
183
184 // Question 2: Log 404s
185 // Validate against whitelist (Bug #13 fix)
186 $q2_answer = isset($_POST['abj404_setup_q2']) ? sanitize_text_field($_POST['abj404_setup_q2']) : 'yes';
187 if (!in_array($q2_answer, self::$allowedQ2Values, true)) {
188 $q2_answer = 'yes'; // Default to safe value
189 }
190
191 $options['capture_404'] = ($q2_answer === 'yes') ? '1' : '0';
192
193 // Question 3: Email alerts
194 $q3_answer = isset($_POST['abj404_setup_q3']) ? sanitize_text_field($_POST['abj404_setup_q3']) : 'yes';
195 if (!in_array($q3_answer, self::$allowedQ3Values, true)) {
196 $q3_answer = 'yes';
197 }
198
199 if ($q3_answer === 'yes') {
200 $options['admin_notification'] = '50';
201 $options['admin_notification_frequency'] = 'weekly';
202 $admin_email = get_option('admin_email');
203 $options['admin_notification_email'] = is_string($admin_email) ? $admin_email : '';
204 }
205
206 // Save options
207 $abj404logic->updateOptions($options);
208 }
209
210 /**
211 * Enqueue assets on 404 Solution admin pages
212 *
213 * @param string $hook Current admin page hook
214 * @return void
215 */
216 public static function enqueueAssets(string $hook): void {
217 // Only load on 404 Solution pages
218 if (!self::isPluginPage()) {
219 return;
220 }
221
222 // Only load if wizard should be shown
223 if (!self::shouldShowWizard()) {
224 return;
225 }
226
227 // Only for users who can manage options
228 if (!current_user_can('manage_options')) {
229 return;
230 }
231
232 // Add inline styles for the modal
233 add_action('admin_head', array(__CLASS__, 'outputStyles'));
234
235 // Output modal HTML in footer
236 add_action('admin_footer', array(__CLASS__, 'outputModalHTML'));
237
238 // Output JavaScript for dismiss functionality
239 add_action('admin_footer', array(__CLASS__, 'outputScript'), 20);
240 }
241
242 /**
243 * Output modal CSS styles
244 * @return void
245 */
246 public static function outputStyles(): void {
247 ?>
248 <style>
249 /* Overlay covers the entire plugin content area including fixed tabs */
250 .abj404-setup-overlay {
251 position: fixed;
252 top: var(--admin-bar-height, 32px);
253 left: 160px; /* WordPress admin menu width */
254 right: 0;
255 bottom: 0;
256 background: rgba(255, 255, 255, 0.85);
257 z-index: 200;
258 display: flex;
259 align-items: flex-start;
260 justify-content: center;
261 padding-top: 50px;
262 }
263
264 /* Adjust for folded menu */
265 @media screen and (max-width: 960px) {
266 .abj404-setup-overlay {
267 left: 36px;
268 }
269 }
270
271 /* Adjust for mobile */
272 @media screen and (max-width: 782px) {
273 .abj404-setup-overlay {
274 left: 0;
275 top: 46px; /* Mobile admin bar height */
276 }
277 }
278
279 .abj404-setup-modal {
280 background: #fff;
281 border-radius: 8px;
282 box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
283 max-width: 500px;
284 width: 95%;
285 max-height: calc(100vh - 150px);
286 overflow-y: auto;
287 position: relative;
288 border: 1px solid #c3c4c7;
289 }
290
291 .abj404-setup-header {
292 background: #2271b1;
293 color: #fff;
294 padding: 16px 20px;
295 border-radius: 8px 8px 0 0;
296 display: flex;
297 justify-content: space-between;
298 align-items: center;
299 }
300
301 .abj404-setup-header h2 {
302 margin: 0;
303 font-size: 18px;
304 font-weight: 600;
305 color: #fff;
306 }
307
308 .abj404-setup-close {
309 background: rgba(255, 255, 255, 0.1);
310 border: 1px solid rgba(255, 255, 255, 0.3);
311 border-radius: 4px;
312 color: #fff;
313 font-size: 20px;
314 cursor: pointer;
315 padding: 2px 8px;
316 line-height: 1;
317 opacity: 0.9;
318 }
319
320 .abj404-setup-close:hover {
321 opacity: 1;
322 background: rgba(255, 255, 255, 0.2);
323 }
324
325 .abj404-setup-close:focus {
326 outline: 2px solid rgba(255, 255, 255, 0.5);
327 outline-offset: 1px;
328 }
329
330 .abj404-setup-content {
331 padding: 16px 24px 10px 24px;
332 }
333
334 .abj404-setup-intro {
335 margin-bottom: 24px;
336 color: #50575e;
337 font-size: 14px;
338 line-height: 1.5;
339 }
340
341 .abj404-setup-question {
342 margin-bottom: 24px;
343 }
344
345 .abj404-setup-question h3 {
346 margin: 0 0 12px 0;
347 font-size: 14px;
348 font-weight: 600;
349 color: #1d2327;
350 }
351
352 .abj404-setup-options {
353 display: flex;
354 flex-direction: column;
355 gap: 8px;
356 }
357
358 .abj404-setup-option {
359 display: flex;
360 align-items: flex-start;
361 padding: 10px 12px;
362 background: #f6f7f7;
363 border-radius: 4px;
364 cursor: pointer;
365 transition: background 0.2s;
366 }
367
368 .abj404-setup-option:hover {
369 background: #eef0f0;
370 }
371
372 .abj404-setup-option:has(input:checked) {
373 background: #e6f2ff;
374 border: 1px solid #2271b1;
375 margin: -1px;
376 }
377
378 .abj404-setup-option:focus-within {
379 outline: 2px solid #2271b1;
380 outline-offset: 1px;
381 }
382
383 .abj404-setup-option input[type="radio"] {
384 margin: 2px 10px 0 0;
385 flex-shrink: 0;
386 accent-color: #2271b1;
387 }
388
389 .abj404-setup-option input[type="radio"],
390 .abj404-setup-option input[type="radio"]:focus,
391 .abj404-setup-option input[type="radio"]:checked,
392 .abj404-setup-option input[type="radio"]:checked:focus {
393 outline: none !important;
394 box-shadow: none !important;
395 border-color: #2271b1 !important;
396 }
397
398 .abj404-setup-option-text {
399 flex: 1;
400 }
401
402 .abj404-setup-option-label {
403 display: block;
404 font-weight: 500;
405 color: #1d2327;
406 margin-bottom: 2px;
407 }
408
409 .abj404-setup-option-desc {
410 display: block;
411 font-size: 12px;
412 color: #646970;
413 }
414
415 .abj404-setup-footer {
416 padding: 16px 24px;
417 background: #f6f7f7;
418 border-radius: 0 0 8px 8px;
419 display: flex;
420 justify-content: space-between;
421 align-items: center;
422 gap: 12px;
423 }
424
425 .abj404-setup-footer .button {
426 padding: 6px 16px;
427 }
428
429 .abj404-setup-skip {
430 background: #f6f7f7;
431 border: 1px solid #c3c4c7;
432 border-radius: 3px;
433 color: #50575e;
434 padding: 6px 16px;
435 font-size: 13px;
436 cursor: pointer;
437 text-decoration: none;
438 line-height: 1.5;
439 }
440
441 .abj404-setup-skip:hover {
442 background: #f0f0f1;
443 border-color: #8c8f94;
444 color: #1d2327;
445 }
446
447 .abj404-setup-skip:focus {
448 outline: 2px solid #2271b1;
449 outline-offset: 1px;
450 }
451
452 .abj404-setup-primary {
453 background: #2271b1 !important;
454 border-color: #2271b1 !important;
455 color: #fff !important;
456 }
457
458 .abj404-setup-primary:hover {
459 background: #135e96 !important;
460 border-color: #135e96 !important;
461 }
462
463 /* Loading overlay */
464 .abj404-setup-loading {
465 position: absolute;
466 top: 0;
467 left: 0;
468 right: 0;
469 bottom: 0;
470 background: rgba(255, 255, 255, 0.9);
471 display: flex;
472 flex-direction: column;
473 align-items: center;
474 justify-content: center;
475 gap: 12px;
476 border-radius: 8px;
477 z-index: 10;
478 }
479
480 /* Toast notification for AJAX errors */
481 .abj404-toast {
482 position: fixed;
483 bottom: 20px;
484 right: 20px;
485 background: #d63638;
486 color: #fff;
487 padding: 12px 16px;
488 border-radius: 4px;
489 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
490 z-index: 9999;
491 max-width: 350px;
492 font-size: 13px;
493 line-height: 1.4;
494 animation: abj404-toast-slide 0.3s ease-out;
495 }
496
497 @keyframes abj404-toast-slide {
498 from {
499 opacity: 0;
500 transform: translateY(20px);
501 }
502 to {
503 opacity: 1;
504 transform: translateY(0);
505 }
506 }
507
508 .abj404-toast-close {
509 background: none;
510 border: none;
511 color: #fff;
512 font-size: 16px;
513 cursor: pointer;
514 float: right;
515 margin: -4px -4px 0 8px;
516 padding: 0 4px;
517 opacity: 0.8;
518 }
519
520 .abj404-toast-close:hover {
521 opacity: 1;
522 }
523
524 body.abj404-dark-mode .abj404-toast {
525 background: #dc3545;
526 }
527
528 .abj404-setup-loading span {
529 color: #1d2327;
530 font-size: 14px;
531 }
532
533 .abj404-setup-spinner {
534 width: 24px;
535 height: 24px;
536 border: 3px solid #c3c4c7;
537 border-top-color: #2271b1;
538 border-radius: 50%;
539 animation: abj404-setup-spin 0.8s linear infinite;
540 }
541
542 @keyframes abj404-setup-spin {
543 to { transform: rotate(360deg); }
544 }
545
546 /* Dark mode support */
547 body.abj404-dark-mode .abj404-setup-overlay {
548 background: rgba(0, 0, 0, 0.75);
549 }
550
551 body.abj404-dark-mode .abj404-setup-modal {
552 background: #1e1e1e;
553 border-color: #3d3d3d;
554 }
555
556 body.abj404-dark-mode .abj404-setup-content {
557 color: #e0e0e0;
558 }
559
560 body.abj404-dark-mode .abj404-setup-intro {
561 color: #b0b0b0;
562 }
563
564 body.abj404-dark-mode .abj404-setup-question h3 {
565 color: #e0e0e0;
566 }
567
568 body.abj404-dark-mode .abj404-setup-option {
569 background: #2d2d2d;
570 }
571
572 body.abj404-dark-mode .abj404-setup-option:hover {
573 background: #3d3d3d;
574 }
575
576 body.abj404-dark-mode .abj404-setup-option:has(input:checked) {
577 background: #1a3a5c;
578 border-color: #5aa2ff;
579 }
580
581 body.abj404-dark-mode .abj404-setup-option:focus-within {
582 outline-color: #5aa2ff;
583 }
584
585 body.abj404-dark-mode .abj404-setup-option input[type="radio"],
586 body.abj404-dark-mode .abj404-setup-option input[type="radio"]:focus,
587 body.abj404-dark-mode .abj404-setup-option input[type="radio"]:checked,
588 body.abj404-dark-mode .abj404-setup-option input[type="radio"]:checked:focus {
589 accent-color: #5aa2ff;
590 border-color: #5aa2ff !important;
591 }
592
593 body.abj404-dark-mode .abj404-setup-option-label {
594 color: #e0e0e0;
595 }
596
597 body.abj404-dark-mode .abj404-setup-option-desc {
598 color: #a0a0a0;
599 }
600
601 body.abj404-dark-mode .abj404-setup-footer {
602 background: #2d2d2d;
603 }
604
605 body.abj404-dark-mode .abj404-setup-skip {
606 background: #3d3d3d;
607 border-color: #505050;
608 color: #b0b0b0;
609 }
610
611 body.abj404-dark-mode .abj404-setup-skip:hover {
612 background: #4d4d4d;
613 border-color: #606060;
614 color: #e0e0e0;
615 }
616
617 body.abj404-dark-mode .abj404-setup-loading {
618 background: rgba(30, 30, 30, 0.9);
619 }
620
621 body.abj404-dark-mode .abj404-setup-loading span {
622 color: #e0e0e0;
623 }
624
625 body.abj404-dark-mode .abj404-setup-spinner {
626 border-color: #505050;
627 border-top-color: #5aa2ff;
628 }
629 </style>
630 <?php
631 }
632
633 /**
634 * Output the modal HTML structure
635 * @return void
636 */
637 public static function outputModalHTML(): void {
638 ?>
639 <div id="abj404-setup-wizard" class="abj404-setup-overlay">
640 <div class="abj404-setup-modal">
641 <form method="post" action="">
642 <?php wp_nonce_field('abj404_setup_wizard', 'abj404_setup_wizard_nonce'); ?>
643
644 <div class="abj404-setup-header">
645 <h2><?php esc_html_e('Welcome to 404 Solution', '404-solution'); ?></h2>
646 <button type="button" id="abj404-setup-close" class="abj404-setup-close" title="<?php esc_attr_e('Close', '404-solution'); ?>">&times;</button>
647 </div>
648
649 <div class="abj404-setup-content">
650 <p class="abj404-setup-intro">
651 <?php esc_html_e('404 Solution helps you automatically handle 404 errors and broken links on your site.', '404-solution'); ?>
652 <?php esc_html_e("Let's configure how it handles missing pages. You can always change these settings later.", '404-solution'); ?>
653 </p>
654
655 <!-- Question 1: What happens when page not found -->
656 <div class="abj404-setup-question">
657 <h3><?php esc_html_e('When a page is not found, what should happen?', '404-solution'); ?></h3>
658 <div class="abj404-setup-options">
659 <label class="abj404-setup-option">
660 <input type="radio" name="abj404_setup_q1" value="redirect" checked>
661 <span class="abj404-setup-option-text">
662 <span class="abj404-setup-option-label"><?php esc_html_e('Automatically redirect to similar page (recommended)', '404-solution'); ?></span>
663 <span class="abj404-setup-option-desc"><?php esc_html_e('When a match is found, redirect visitors automatically', '404-solution'); ?></span>
664 </span>
665 </label>
666 <label class="abj404-setup-option">
667 <input type="radio" name="abj404_setup_q1" value="default">
668 <span class="abj404-setup-option-text">
669 <span class="abj404-setup-option-label"><?php esc_html_e('Just show the default 404 page', '404-solution'); ?></span>
670 <span class="abj404-setup-option-desc"><?php esc_html_e("Use WordPress's standard \"Page not found\" screen. Manual redirects still work.", '404-solution'); ?></span>
671 </span>
672 </label>
673 </div>
674 </div>
675
676 <!-- Question 2: Log 404s -->
677 <div class="abj404-setup-question">
678 <h3><?php esc_html_e('Log 404 errors for review?', '404-solution'); ?></h3>
679 <div class="abj404-setup-options">
680 <label class="abj404-setup-option">
681 <input type="radio" name="abj404_setup_q2" value="yes" checked>
682 <span class="abj404-setup-option-text">
683 <span class="abj404-setup-option-label"><?php esc_html_e('Yes, log 404 errors', '404-solution'); ?></span>
684 <span class="abj404-setup-option-desc"><?php esc_html_e('Track missing pages so you can create redirects later', '404-solution'); ?></span>
685 </span>
686 </label>
687 <label class="abj404-setup-option">
688 <input type="radio" name="abj404_setup_q2" value="no">
689 <span class="abj404-setup-option-text">
690 <span class="abj404-setup-option-label"><?php esc_html_e("No, don't log 404s", '404-solution'); ?></span>
691 <span class="abj404-setup-option-desc"><?php esc_html_e('Only handle manually created redirects', '404-solution'); ?></span>
692 </span>
693 </label>
694 </div>
695 </div>
696
697 <!-- Question 3: Email alerts -->
698 <div class="abj404-setup-question">
699 <h3><?php esc_html_e('Get email alerts about 404 problems?', '404-solution'); ?></h3>
700 <div class="abj404-setup-options">
701 <label class="abj404-setup-option">
702 <input type="radio" name="abj404_setup_q3" value="yes" checked>
703 <span class="abj404-setup-option-text">
704 <span class="abj404-setup-option-label"><?php esc_html_e('Yes, email me a weekly summary (recommended)', '404-solution'); ?></span>
705 <span class="abj404-setup-option-desc"><?php esc_html_e('Get notified when captured 404 URLs exceed 50', '404-solution'); ?></span>
706 </span>
707 </label>
708 <label class="abj404-setup-option">
709 <input type="radio" name="abj404_setup_q3" value="no">
710 <span class="abj404-setup-option-text">
711 <span class="abj404-setup-option-label"><?php esc_html_e("No, I'll check manually", '404-solution'); ?></span>
712 <span class="abj404-setup-option-desc"><?php esc_html_e('You can always enable email alerts later in Options', '404-solution'); ?></span>
713 </span>
714 </label>
715 </div>
716 </div>
717 </div>
718
719 <div class="abj404-setup-footer">
720 <button type="button" id="abj404-setup-skip" class="abj404-setup-skip">
721 <?php esc_html_e('Skip Setup', '404-solution'); ?>
722 </button>
723 <!-- Hidden input ensures action is sent even if button is disabled during submit -->
724 <input type="hidden" name="abj404_setup_wizard_action" value="save">
725 <button type="submit" class="button abj404-setup-primary">
726 <?php esc_html_e('Save & Get Started', '404-solution'); ?>
727 </button>
728 </div>
729 </form>
730 </div>
731 </div>
732 <?php
733 }
734
735 /**
736 * Output JavaScript for dismiss and save functionality
737 * @return void
738 */
739 public static function outputScript(): void {
740 ?>
741 <script>
742 (function() {
743 var overlay = document.getElementById('abj404-setup-wizard');
744 var closeBtn = document.getElementById('abj404-setup-close');
745 var skipBtn = document.getElementById('abj404-setup-skip');
746 var saveBtn = document.querySelector('.abj404-setup-primary');
747 var form = document.querySelector('#abj404-setup-wizard form');
748
749 // Bug #9 fix: Null check for nonce element
750 var nonceEl = document.getElementById('abj404_setup_wizard_nonce');
751 var nonce = nonceEl ? nonceEl.value : '';
752
753 // Bug #26 fix: Track dismiss state to prevent multiple calls
754 var isDismissing = false;
755 var isSubmitting = false;
756
757 function dismissWizard() {
758 // Bug #26 fix: Prevent multiple rapid dismissals
759 if (isDismissing) {
760 return;
761 }
762 isDismissing = true;
763
764 // Disable buttons to prevent further clicks
765 if (closeBtn) closeBtn.disabled = true;
766 if (skipBtn) skipBtn.disabled = true;
767
768 // Remove modal immediately
769 if (overlay) {
770 overlay.remove();
771 }
772
773 // Bug #9 fix: Don't send AJAX if no nonce
774 if (!nonce) {
775 showToast(<?php echo wp_json_encode(__('Could not save settings - missing security token. The wizard may appear again on next visit.', '404-solution')); ?>);
776 return;
777 }
778
779 // Fire AJAX to mark as complete with error handling
780 var xhr = new XMLHttpRequest();
781 xhr.open('POST', ajaxurl, true);
782 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
783 xhr.onload = function() {
784 if (xhr.status !== 200) {
785 showToast(<?php echo wp_json_encode(__('Could not save dismissal. The wizard may appear again on next visit.', '404-solution')); ?>);
786 return;
787 }
788 try {
789 var response = JSON.parse(xhr.responseText);
790 if (!response.success) {
791 var msg = response.data && response.data.message ? response.data.message : '';
792 showToast(<?php echo wp_json_encode(__('Could not save dismissal: ', '404-solution')); ?> + msg + <?php echo wp_json_encode(__(' The wizard may appear again on next visit.', '404-solution')); ?>);
793 }
794 } catch (e) {
795 showToast(<?php echo wp_json_encode(__('Could not save dismissal. The wizard may appear again on next visit.', '404-solution')); ?>);
796 }
797 };
798 xhr.onerror = function() {
799 showToast(<?php echo wp_json_encode(__('Network error - could not save dismissal. The wizard may appear again on next visit.', '404-solution')); ?>);
800 };
801 xhr.send('action=abj404_dismiss_setup_wizard&nonce=' + encodeURIComponent(nonce));
802 }
803
804 function showToast(message) {
805 // Remove any existing toast
806 var existingToast = document.querySelector('.abj404-toast');
807 if (existingToast) {
808 existingToast.remove();
809 }
810
811 // Create toast element using DOM methods
812 var toast = document.createElement('div');
813 toast.className = 'abj404-toast';
814 toast.setAttribute('role', 'alert');
815
816 var closeBtn = document.createElement('button');
817 closeBtn.className = 'abj404-toast-close';
818 closeBtn.setAttribute('aria-label', <?php echo wp_json_encode(__('Close', '404-solution')); ?>);
819 closeBtn.textContent = '\u00D7';
820 closeBtn.onclick = function() { toast.remove(); };
821
822 var textNode = document.createTextNode(message);
823
824 toast.appendChild(closeBtn);
825 toast.appendChild(textNode);
826 document.body.appendChild(toast);
827
828 // Auto-remove after 10 seconds
829 setTimeout(function() {
830 if (toast && toast.parentNode) {
831 toast.remove();
832 }
833 }, 10000);
834 }
835
836 function showSavingOverlay() {
837 // Bug #26 fix: Prevent multiple submissions
838 if (isSubmitting) {
839 return false;
840 }
841 isSubmitting = true;
842
843 // Disable buttons
844 if (saveBtn) saveBtn.disabled = true;
845 if (skipBtn) skipBtn.disabled = true;
846 if (closeBtn) closeBtn.disabled = true;
847
848 // Bug #19 fix: Use DOM methods instead of innerHTML
849 var loadingOverlay = document.createElement('div');
850 loadingOverlay.className = 'abj404-setup-loading';
851
852 var spinner = document.createElement('div');
853 spinner.className = 'abj404-setup-spinner';
854 loadingOverlay.appendChild(spinner);
855
856 var loadingText = document.createElement('span');
857 loadingText.textContent = <?php echo wp_json_encode(__('Saving...', '404-solution')); ?>;
858 loadingOverlay.appendChild(loadingText);
859
860 var modal = overlay ? overlay.querySelector('.abj404-setup-modal') : null;
861 if (modal) {
862 modal.appendChild(loadingOverlay);
863 }
864 }
865
866 if (closeBtn) {
867 closeBtn.addEventListener('click', dismissWizard);
868 }
869 if (skipBtn) {
870 skipBtn.addEventListener('click', dismissWizard);
871 }
872 if (form) {
873 form.addEventListener('submit', showSavingOverlay);
874 }
875 })();
876 </script>
877 <?php
878 }
879 }
880