PluginProbe
WebberZone Top 10 — Popular Posts / 4.3.4
WebberZone Top 10 — Popular Posts v4.3.4
4.5.1 4.5.0 4.4.3 4.4.2 4.4.1 4.4.0 4.3.4 4.3.3 4.3.2 4.3.1 4.3.0 trunk 1.0 1.0.1 1.1 1.2 1.3 1.4 1.4.1 1.5 1.5.1 1.5.2 1.5.3 1.6 1.6.1 All 117 releases
top-10 / includes / admin / settings / class-settings-wizard-api.php

class-settings-wizard-api.php in WebberZone Top 10 — Popular Posts 4.3.4, at includes/admin/settings/class-settings-wizard-api.php

910 lines 25.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Settings Wizard API.
4 *
5 * A reusable API class for creating multi-step settings wizards.
6 * This class provides the framework for creating guided setup experiences.
7 *
8 * @package WebberZone\Top_Ten
9 */
10
11 namespace WebberZone\Top_Ten\Admin\Settings;
12
13 use WebberZone\Top_Ten\Admin\Settings\Settings_Sanitize;
14 use WebberZone\Top_Ten\Admin\Settings\Settings_API;
15
16 // If this file is called directly, abort.
17 if ( ! defined( 'WPINC' ) ) {
18 die;
19 }
20
21 /**
22 * Settings Wizard API class
23 */
24 class Settings_Wizard_API {
25
26 /**
27 * Current version number
28 *
29 * @var string
30 */
31 public const VERSION = Settings_API::VERSION;
32
33 /**
34 * Settings sanitizer instance.
35 *
36 * @var Settings_Sanitize
37 */
38 protected $settings_sanitize;
39
40 /**
41 * Settings Key.
42 *
43 * @var string Settings Key.
44 */
45 public $settings_key;
46
47 /**
48 * Prefix which is used for creating the unique filters and actions.
49 *
50 * @var string Prefix.
51 */
52 public $prefix;
53
54 /**
55 * Menu arguments for the wizard.
56 *
57 * @var array Menu arguments array with parent and capability.
58 */
59 protected $menu_args;
60
61 /**
62 * Wizard steps configuration.
63 *
64 * @var array Wizard steps.
65 */
66 protected $steps = array();
67
68 /**
69 * Current step number.
70 *
71 * @var int Current step.
72 */
73 protected $current_step = 1;
74
75 /**
76 * Total number of steps.
77 *
78 * @var int Total steps.
79 */
80 protected $total_steps = 0;
81
82 /**
83 * Translation strings.
84 *
85 * @var array Translation strings.
86 */
87 public $translation_strings;
88
89 /**
90 * Wizard page slug.
91 *
92 * @var string Wizard page slug.
93 */
94 public $page_slug;
95
96 /**
97 * Wizard page id.
98 *
99 * @var string Wizard page id.
100 */
101 public $page_id;
102
103 /**
104 * Settings form.
105 *
106 * @var object Settings form.
107 */
108 public $settings_form;
109
110 /**
111 * Args.
112 *
113 * @var array Args.
114 */
115 public $args;
116
117 /**
118 * Main constructor class.
119 *
120 * @param string $settings_key Settings key.
121 * @param string $prefix Prefix. Used for actions and filters.
122 * @param array $args {
123 * Array of arguments.
124 * @type array $steps Array of wizard steps.
125 * @type array $translation_strings Translation strings.
126 * @type string $page_slug Wizard page slug.
127 * @type array $menu_args Menu arguments array with parent and capability.
128 * @type bool $hide_when_completed Whether to hide the wizard submenu item after completion.
129 * @type bool $show_in_menu Whether to show the wizard in the admin menu.
130 * }
131 */
132 public function __construct( $settings_key, $prefix, $args = array() ) {
133
134 $this->settings_key = $settings_key;
135 $this->prefix = $prefix;
136
137 $defaults = array(
138 'steps' => array(),
139 'translation_strings' => array(),
140 'admin_menu_position' => 999,
141 'page_slug' => "{$prefix}_wizard",
142 'hide_when_completed' => true,
143 'show_in_menu' => true,
144 'menu_args' => array(
145 'parent' => '', // Empty for dashboard, or parent slug for submenu.
146 'capability' => 'manage_options',
147 ),
148 );
149 $args = wp_parse_args( $args, $defaults );
150 $this->args = $args;
151
152 $this->page_slug = $args['page_slug'];
153 $this->menu_args = $args['menu_args'];
154 $this->set_translation_strings( $args['translation_strings'] );
155 $this->set_steps( $args['steps'] );
156
157 // Initialize settings form.
158 $this->settings_form = new Settings_Form(
159 array(
160 'settings_key' => $this->settings_key,
161 'prefix' => $this->prefix,
162 'translation_strings' => $this->translation_strings,
163 )
164 );
165
166 $this->settings_sanitize = new Settings_Sanitize(
167 array(
168 'settings_key' => $this->settings_key,
169 'prefix' => $this->prefix,
170 )
171 );
172
173 $this->hooks();
174 }
175
176 /**
177 * Adds the functions to the appropriate WordPress hooks.
178 */
179 public function hooks() {
180 add_action( 'admin_menu', array( $this, 'admin_menu' ), $this->args['admin_menu_position'] );
181 add_action( 'admin_init', array( $this, 'process_step' ) );
182 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
183 }
184
185 /**
186 * Sets translation strings.
187 *
188 * @param array $strings Translation strings.
189 */
190 public function set_translation_strings( $strings ) {
191 $defaults = array(
192 'page_title' => 'Setup Wizard',
193 'menu_title' => 'Setup Wizard',
194 'wizard_title' => 'Setup Wizard',
195 'next_step' => 'Next Step',
196 'previous_step' => 'Previous Step',
197 'finish_setup' => 'Finish Setup',
198 'skip_wizard' => 'Skip Wizard',
199 'step_of' => 'Step %1$d of %2$d',
200 'steps_nav_aria_label' => 'Setup Wizard Steps',
201 'wizard_complete' => 'Wizard Complete!',
202 'setup_complete' => 'Setup has been completed successfully.',
203 'go_to_settings' => 'Go to Settings',
204 'tom_select_no_results' => 'No results found for "%s"',
205 );
206
207 $this->translation_strings = wp_parse_args( $strings, $defaults );
208 }
209
210 /**
211 * Set wizard steps.
212 *
213 * @param array $steps Array of wizard steps.
214 * @return object Class object.
215 */
216 public function set_steps( $steps ) {
217 $this->steps = $steps;
218 $this->total_steps = count( $steps );
219 return $this;
220 }
221
222 /**
223 * Add admin menu for the wizard.
224 */
225 public function admin_menu() {
226 $capability = ! empty( $this->menu_args['capability'] ) ? $this->menu_args['capability'] : 'manage_options';
227 $parent = ! empty( $this->menu_args['parent'] ) ? $this->menu_args['parent'] : 'index.php';
228
229 $this->page_id = add_submenu_page(
230 $parent,
231 (string) $this->translation_strings['page_title'],
232 (string) $this->translation_strings['menu_title'],
233 $capability,
234 $this->page_slug,
235 array( $this, 'render_wizard_page' )
236 );
237
238 $hide_submenu = ( isset( $this->args['show_in_menu'] ) && ! $this->args['show_in_menu'] ) ||
239 ( ( $this->args['hide_when_completed'] ?? true ) && $this->is_wizard_completed() );
240
241 if ( $hide_submenu ) {
242 add_action( 'admin_enqueue_scripts', array( $this, 'hide_completed_wizard_submenu' ) );
243 }
244 }
245
246 /**
247 * Hide wizard submenu item when the wizard is completed.
248 *
249 * @return void
250 */
251 public function hide_completed_wizard_submenu() {
252 $slug = sanitize_key( $this->page_slug );
253 $css = '#adminmenu a[href$="page=' . $slug . '"],
254 #adminmenu a[href*="page=' . $slug . '&"] {
255 display: none;
256 }';
257 wp_add_inline_style( 'wp-admin', $css );
258 }
259
260 /**
261 * Enqueue scripts and styles for the wizard.
262 *
263 * @param string $hook Current admin page hook.
264 */
265 public function enqueue_scripts( $hook ) {
266 if ( false === strpos( $hook, $this->page_slug ) ) {
267 return;
268 }
269
270 $minimize = ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? '' : '.min';
271
272 // Wizard styles.
273 wp_enqueue_style(
274 "{$this->prefix}-wizard-css",
275 plugins_url( 'css/wizard' . $minimize . '.css', __FILE__ ),
276 array( 'wp-color-picker' ),
277 $this->get_version(),
278 'all'
279 );
280
281 // Use Settings_API to enqueue common scripts and styles.
282 Settings_API::enqueue_scripts_styles( $this->prefix );
283
284 // Tom Select assets for taxonomy fields.
285 wp_register_style(
286 'wz-' . $this->prefix . '-tom-select',
287 plugins_url( 'css/tom-select.min.css', __FILE__ ),
288 array(),
289 $this->get_version()
290 );
291 wp_register_script(
292 'wz-' . $this->prefix . '-tom-select',
293 plugins_url( 'js/tom-select.complete.min.js', __FILE__ ),
294 array( 'jquery' ),
295 $this->get_version(),
296 true
297 );
298 wp_register_script(
299 'wz-' . $this->prefix . '-tom-select-init',
300 plugin_dir_url( __FILE__ ) . 'js/tom-select-init' . $minimize . '.js',
301 array( 'jquery', 'wz-' . $this->prefix . '-tom-select' ),
302 $this->get_version(),
303 true
304 );
305 wp_enqueue_style( 'wz-' . $this->prefix . '-tom-select' );
306 wp_enqueue_script( 'wz-' . $this->prefix . '-tom-select' );
307 wp_enqueue_script( 'wz-' . $this->prefix . '-tom-select-init' );
308
309 // Localize Tom Select settings for wizard.
310 wp_localize_script(
311 'wz-' . $this->prefix . '-tom-select-init',
312 "{$this->prefix}TomSelectSettings",
313 array(
314 'action' => $this->prefix . '_taxonomy_search_tom_select',
315 'nonce' => wp_create_nonce( $this->prefix . '_taxonomy_search_tom_select' ),
316 'endpoint' => 'category',
317 'strings' => array(
318 'no_results' => esc_html( $this->translation_strings['tom_select_no_results'] ),
319 ),
320 )
321 );
322 }
323
324 /**
325 * Process wizard step submission.
326 */
327 public function process_step() {
328 if ( empty( $_POST['wizard_action'] ) ) { // Don't run on every admin_init, only on our form submission.
329 return;
330 }
331
332 $nonce_value = isset( $_POST[ $this->prefix . '_wizard_nonce' ] ) ? sanitize_text_field( wp_unslash( $_POST[ $this->prefix . '_wizard_nonce' ] ) ) : '';
333 if ( empty( $nonce_value ) || ! wp_verify_nonce( $nonce_value, $this->prefix . '_wizard_nonce' ) ) {
334 return;
335 }
336
337 if ( ! current_user_can( 'manage_options' ) ) {
338 return;
339 }
340
341 // Initialise the current step based on the URL or stored option before processing the action.
342 $this->current_step = $this->get_current_step();
343
344 $action = sanitize_text_field( wp_unslash( $_POST['wizard_action'] ) );
345
346 switch ( $action ) {
347 case 'next_step':
348 $this->process_current_step();
349 $this->next_step();
350 $this->redirect_to_step( $this->current_step );
351 break;
352
353 case 'previous_step':
354 $this->previous_step();
355 $this->redirect_to_step( $this->current_step );
356 break;
357
358 case 'finish_setup':
359 $this->process_current_step();
360 $this->mark_wizard_completed();
361 $this->redirect_to_step( $this->total_steps + 1 );
362 break;
363
364 case 'skip_wizard':
365 $this->mark_wizard_completed();
366 $this->redirect_to_admin();
367 break;
368 default:
369 break;
370 }
371 }
372
373 /**
374 * Process the current step's form data.
375 */
376 protected function process_current_step() {
377 $current_step_config = $this->get_current_step_config();
378
379 if ( empty( $current_step_config['settings'] ) ) {
380 return;
381 }
382
383 $settings = array();
384
385 // phpcs:ignore WordPress.Security.NonceVerification.Missing
386 if ( isset( $_POST[ $this->settings_key ] ) && is_array( $_POST[ $this->settings_key ] ) ) {
387 foreach ( $current_step_config['settings'] as $setting_id => $setting_config ) {
388 // phpcs:ignore WordPress.Security.NonceVerification.Missing
389 if ( isset( $_POST[ $this->settings_key ][ $setting_id ] ) ) {
390 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.NonceVerification.Missing
391 $value = $this->sanitize_setting_value( wp_unslash( $_POST[ $this->settings_key ][ $setting_id ] ), $setting_config );
392 $settings[ $setting_id ] = $value;
393 }
394 }
395 }
396
397 // Save settings for this step.
398 $this->save_step_settings( $settings );
399
400 /**
401 * Action fired after processing a wizard step.
402 *
403 * @param int $step Current step number.
404 * @param array $settings Settings data for this step.
405 */
406 do_action( $this->prefix . '_wizard_step_processed', $this->current_step, $settings ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
407 }
408
409 /**
410 * Sanitize setting value based on its type.
411 *
412 * @param mixed $value Setting value.
413 * @param array $setting_config Setting configuration.
414 * @return mixed Sanitized value.
415 */
416 protected function sanitize_setting_value( $value, $setting_config ) {
417 $type = $setting_config['type'] ?? 'text';
418
419 // Use the Settings_Sanitize class for proper sanitization.
420 $settings_sanitize = $this->settings_sanitize;
421
422 // Check if we have a specific sanitizer for this type.
423 if ( is_callable( array( $settings_sanitize, "sanitize_{$type}_field" ) ) ) {
424 return call_user_func( array( $settings_sanitize, "sanitize_{$type}_field" ), $value );
425 }
426
427 // Fallback to basic sanitization.
428 if ( is_array( $value ) ) {
429 return array_map( 'sanitize_text_field', $value );
430 }
431
432 return sanitize_text_field( $value );
433 }
434
435 /**
436 * Save settings for the current step.
437 *
438 * @param array $settings Settings to save.
439 */
440 protected function save_step_settings( $settings ) {
441 $existing_settings = get_option( $this->settings_key, array() );
442 $updated_settings = array_merge( $existing_settings, $settings );
443 update_option( $this->settings_key, $updated_settings );
444 }
445
446 /**
447 * Move to the next step.
448 */
449 protected function next_step() {
450 if ( $this->current_step < $this->total_steps ) {
451 ++$this->current_step;
452 $this->update_current_step();
453 }
454 }
455
456 /**
457 * Move to the previous step.
458 */
459 protected function previous_step() {
460 if ( $this->current_step > 1 ) {
461 --$this->current_step;
462 $this->update_current_step();
463 }
464 }
465
466 /**
467 * Redirect to a specific wizard step.
468 *
469 * @param int $step Step number to redirect to.
470 */
471 protected function redirect_to_step( $step ) {
472 $url = add_query_arg(
473 array(
474 'page' => $this->page_slug,
475 'step' => $step,
476 ),
477 admin_url( 'admin.php' )
478 );
479 wp_safe_redirect( $url );
480 exit;
481 }
482
483 /**
484 * Redirect to the admin page after wizard completion.
485 */
486 protected function redirect_to_admin() {
487 $url = $this->get_completion_redirect_url();
488 wp_safe_redirect( $url );
489 exit;
490 }
491
492 /**
493 * Mark the wizard as completed without redirecting.
494 */
495 protected function mark_wizard_completed() {
496 update_option( "{$this->prefix}_wizard_completed", true );
497 update_option( "{$this->prefix}_wizard_completed_date", current_time( 'mysql' ) );
498
499 // Clean up the transient and option that triggered the wizard.
500 delete_transient( "{$this->prefix}_show_wizard_activation_redirect" );
501 delete_option( "{$this->prefix}_show_wizard" );
502
503 /**
504 * Action fired when the wizard is completed.
505 *
506 * @param string $prefix Plugin prefix.
507 */
508 do_action( "{$this->prefix}_wizard_completed", $this->prefix ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
509 }
510
511 /**
512 * Complete the wizard.
513 */
514 protected function complete_wizard() {
515 $this->mark_wizard_completed();
516
517 // Redirect to completion page or main settings.
518 wp_safe_redirect( $this->get_completion_redirect_url() );
519 exit;
520 }
521
522 /**
523 * Get the current step number.
524 *
525 * @return int Current step number.
526 */
527 public function get_current_step() {
528 // Check if we have a step parameter in the URL first.
529 // phpcs:disable WordPress.Security.NonceVerification.Recommended
530 if ( isset( $_GET['step'] ) ) {
531 $step = absint( $_GET['step'] );
532 if ( $step > 0 && $step <= $this->total_steps + 1 ) {
533 $this->current_step = $step;
534 $this->update_current_step();
535 return $this->current_step;
536 }
537 }
538 // phpcs:enable WordPress.Security.NonceVerification.Recommended
539
540 // Fall back to the database value.
541 if ( ! $this->current_step ) {
542 $this->current_step = get_option( "{$this->prefix}_wizard_current_step", 1 );
543 }
544 return $this->current_step;
545 }
546
547 /**
548 * Update the current step in the database.
549 */
550 protected function update_current_step() {
551 update_option( "{$this->prefix}_wizard_current_step", $this->current_step );
552 }
553
554 /**
555 * Get the current step configuration.
556 *
557 * @return array Current step configuration.
558 */
559 public function get_current_step_config() {
560 $keys = array_keys( $this->steps );
561 $index = $this->get_current_step() - 1;
562
563 // Return empty array if steps is empty or index is out of bounds.
564 if ( empty( $keys ) || ! isset( $keys[ $index ] ) ) {
565 return array();
566 }
567
568 return $this->steps[ $keys[ $index ] ] ?? array();
569 }
570
571 /**
572 * Check if the wizard has been completed.
573 *
574 * @return bool True if wizard is completed.
575 */
576 public function is_wizard_completed() {
577 return (bool) get_option( "{$this->prefix}_wizard_completed", false );
578 }
579
580 /**
581 * Render the wizard page.
582 */
583 public function render_wizard_page() {
584
585 $this->current_step = $this->get_current_step();
586 $step_config = $this->get_current_step_config();
587
588 if ( empty( $step_config ) ) {
589 $this->render_completion_page();
590 return;
591 }
592
593 ?>
594 <div class="wrap wizard-wrap">
595 <h1><?php echo esc_html( $this->translation_strings['wizard_title'] ); ?></h1>
596
597 <?php $this->render_wizard_steps_navigation(); ?>
598
599 <div class="wizard-progress">
600 <div class="wizard-progress-bar">
601 <div class="wizard-progress-fill" style="width: <?php echo esc_attr( (string) ( ( $this->current_step / $this->total_steps ) * 100 ) ); ?>%;"></div>
602 </div>
603 <p class="wizard-step-counter">
604 <?php
605 $current_step_name = $step_config['title'] ?? '';
606 $step_pattern = ! empty( $current_step_name ) ? '%1$s - Step %2$d of %3$d' : $this->translation_strings['step_of'];
607 printf(
608 esc_html( $step_pattern ),
609 esc_html( $current_step_name ),
610 esc_html( (string) $this->current_step ),
611 esc_html( (string) $this->total_steps )
612 );
613 ?>
614 </p>
615 </div>
616
617 <div class="wizard-content">
618 <div class="wizard-step">
619 <h2><?php echo esc_html( $step_config['title'] ?? '' ); ?></h2>
620
621 <?php if ( ! empty( $step_config['description'] ) ) : ?>
622 <p class="wizard-step-description"><?php echo wp_kses_post( $step_config['description'] ); ?></p>
623 <?php endif; ?>
624
625 <form method="post" action="">
626 <?php wp_nonce_field( "{$this->prefix}_wizard_nonce", "{$this->prefix}_wizard_nonce" ); ?>
627
628 <div class="wizard-fields">
629 <?php if ( ! empty( $step_config['settings'] ) ) : ?>
630 <table class="form-table">
631 <?php
632 foreach ( $step_config['settings'] as $setting_id => $field ) {
633 $args = Settings_API::parse_field_args( $field );
634
635 // Get all settings from the main settings array.
636 $all_settings = get_option( $this->settings_key, array() );
637
638 // Check if this setting exists in the saved settings.
639 $value = $all_settings[ $setting_id ] ?? null;
640
641 // Use saved value if it exists, otherwise use default.
642 $args['value'] = ( null !== $value ) ? $value : ( $args['default'] ?? '' );
643 $type = $args['type'] ?? 'text';
644 $callback = method_exists( $this->settings_form, "callback_{$type}" ) ? array( $this->settings_form, "callback_{$type}" ) : array( $this->settings_form, 'callback_missing' );
645
646 echo '<tr>';
647 echo '<th scope="row">';
648 if ( ! empty( $args['name'] ) ) {
649 echo '<label for="' . esc_attr( $setting_id ) . '">' . esc_html( wp_strip_all_tags( $args['name'] ) ) . '</label>';
650 }
651 echo '</th>';
652 echo '<td>';
653 \call_user_func( $callback, $args );
654 echo '</td>';
655 echo '</tr>';
656 }
657 ?>
658 </table>
659 <?php endif; ?>
660 </div>
661
662 <?php
663 /**
664 * Fires before the wizard actions are rendered.
665 *
666 * @param int $current_step Current step number.
667 * @param int $total_steps Total number of steps.
668 */
669 do_action( "{$this->prefix}_wizard_before_actions", $this->current_step, $this->total_steps ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
670 ?>
671
672 <div class="wizard-actions">
673 <?php $this->render_wizard_buttons(); ?>
674 </div>
675 </form>
676 </div>
677 </div>
678 </div>
679 <?php
680 }
681
682 /**
683 * Get the current value for a setting.
684 *
685 * @param string $setting_id Setting ID.
686 * @return mixed Setting value.
687 */
688 protected function get_setting_value( $setting_id ) {
689 $settings = get_option( $this->settings_key, array() );
690 return $settings[ $setting_id ] ?? '';
691 }
692
693 /**
694 * Get the skip wizard link URL.
695 *
696 * @return string Skip wizard link URL.
697 */
698 protected function get_skip_link_url() {
699 return $this->get_completion_redirect_url();
700 }
701
702 /**
703 * Render wizard navigation buttons.
704 */
705 protected function render_wizard_buttons() {
706 ?>
707 <div class="wizard-button-group">
708 <?php if ( $this->current_step > 1 ) : ?>
709 <button type="submit" name="wizard_action" value="previous_step" class="button button-secondary">
710 <?php echo esc_html( $this->translation_strings['previous_step'] ); ?>
711 </button>
712 <?php endif; ?>
713
714 <?php if ( $this->current_step < $this->total_steps ) : ?>
715 <button type="submit" name="wizard_action" value="next_step" class="button button-primary">
716 <?php echo esc_html( $this->translation_strings['next_step'] ); ?>
717 </button>
718 <?php else : ?>
719 <button type="submit" name="wizard_action" value="finish_setup" class="button button-primary">
720 <?php echo esc_html( $this->translation_strings['finish_setup'] ); ?>
721 </button>
722 <?php endif; ?>
723
724 <button type="submit" name="wizard_action" value="skip_wizard" class="button wizard-button-skip">
725 <?php echo esc_html( $this->translation_strings['skip_wizard'] ); ?>
726 </button>
727 </div>
728 <?php
729 }
730
731 /**
732 * Render the completion page.
733 */
734 protected function render_completion_page() {
735 /**
736 * Fires before the wizard completion page content.
737 */
738 do_action( "{$this->prefix}_wizard_completion_before" ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
739 ?>
740 <div class="wrap wizard-wrap wizard-complete">
741 <h1><?php echo esc_html( $this->translation_strings['wizard_complete'] ); ?></h1>
742 <p><?php echo esc_html( $this->translation_strings['setup_complete'] ); ?></p>
743
744 <?php
745 /**
746 * Fires after the wizard completion message.
747 */
748 do_action( "{$this->prefix}_wizard_completion_message" ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
749 ?>
750
751 <p class="wizard-actions">
752 <?php
753 $buttons = $this->get_completion_buttons();
754 foreach ( $buttons as $button ) :
755 $class = isset( $button['primary'] ) && $button['primary'] ? 'button-primary' : 'button-secondary';
756 ?>
757 <a href="<?php echo esc_url( $button['url'] ); ?>" class="button <?php echo esc_attr( $class ); ?>">
758 <?php echo esc_html( $button['text'] ); ?>
759 </a>
760 <?php endforeach; ?>
761 </p>
762 </div>
763 <?php
764 /**
765 * Fires after the wizard completion page content.
766 */
767 do_action( "{$this->prefix}_wizard_completion_after" ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
768 }
769
770 /**
771 * Get the URL to redirect to after wizard completion.
772 *
773 * @return string Redirect URL.
774 */
775 protected function get_completion_redirect_url() {
776 /**
777 * Filter the URL to redirect to after wizard completion.
778 *
779 * @param string $url The URL to redirect to.
780 * @param string $prefix Plugin prefix.
781 */
782 return apply_filters( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
783 "{$this->prefix}_wizard_completion_url", // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
784 admin_url( "admin.php?page={$this->prefix}_settings" ),
785 $this->prefix
786 );
787 }
788
789 /**
790 * Get the completion page buttons.
791 *
792 * @return array Array of button configurations.
793 */
794 protected function get_completion_buttons() {
795 $buttons = array(
796 array(
797 'url' => $this->get_completion_redirect_url(),
798 'text' => $this->translation_strings['go_to_settings'],
799 'primary' => true,
800 ),
801 );
802
803 /**
804 * Filter the completion page buttons.
805 *
806 * @param array $buttons Array of button configurations.
807 * @param string $prefix Plugin prefix.
808 */
809 return apply_filters( "{$this->prefix}_wizard_completion_buttons", $buttons, $this->prefix ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
810 }
811
812 /**
813 * Get the version for cache busting.
814 *
815 * @return string Version number.
816 */
817 protected function get_version() {
818 /**
819 * Filter the version number used for cache busting.
820 *
821 * @param string $version Version number.
822 * @param string $prefix Plugin prefix.
823 */
824 return apply_filters( "{$this->prefix}_wizard_version", self::VERSION, $this->prefix ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound
825 }
826
827 /**
828 * Render the wizard steps navigation.
829 */
830 protected function render_wizard_steps_navigation() {
831 $step_keys = array_keys( $this->steps );
832 ?>
833 <ol class="wizard-steps-nav" aria-label="<?php echo esc_attr( $this->translation_strings['steps_nav_aria_label'] ?? 'Setup Wizard Steps' ); ?>">
834 <?php
835 foreach ( $step_keys as $index => $step_key ) :
836 $step_number = $index + 1;
837 $step_config = $this->steps[ $step_key ];
838 $is_current = $step_number === $this->current_step;
839 $is_completed = $step_number < $this->current_step;
840 $class_parts = array();
841
842 if ( $is_current ) {
843 $class_parts[] = 'active';
844 } elseif ( $is_completed ) {
845 $class_parts[] = 'done';
846 }
847
848 $class = implode( ' ', $class_parts );
849 ?>
850 <li class="<?php echo esc_attr( $class ); ?>"<?php echo $is_current ? ' aria-current="step"' : ''; ?>>
851 <?php if ( $is_completed ) : ?>
852 <a href="<?php echo esc_url( $this->get_step_url( $step_number ) ); ?>" class="step-link">
853 <span class="step-number"><?php echo esc_html( (string) $step_number ); ?></span>
854 <span class="step-name"><?php echo esc_html( $step_config['title'] ?? '' ); ?></span>
855 </a>
856 <?php else : ?>
857 <span class="step-number"><?php echo esc_html( (string) $step_number ); ?></span>
858 <span class="step-name"><?php echo esc_html( $step_config['title'] ?? '' ); ?></span>
859 <?php endif; ?>
860 </li>
861 <?php
862 endforeach;
863 ?>
864 </ol>
865 <?php
866 }
867
868 /**
869 * Get the URL for a specific wizard step.
870 *
871 * @param int $step Step number.
872 * @return string Step URL.
873 */
874 protected function get_step_url( $step ) {
875 return add_query_arg(
876 array(
877 'page' => $this->page_slug,
878 'step' => $step,
879 ),
880 admin_url( 'admin.php' )
881 );
882 }
883
884 /**
885 * Reset the wizard to allow it to run again.
886 */
887 public function reset_wizard() {
888 delete_option( "{$this->prefix}_wizard_completed" );
889 delete_option( "{$this->prefix}_wizard_completed_date" );
890 delete_option( "{$this->prefix}_wizard_current_step" );
891 }
892
893 /**
894 * Check if the wizard should be shown (e.g., on first activation).
895 *
896 * @return bool True if wizard should be shown.
897 */
898 public function should_show_wizard() {
899 // Show wizard if it hasn't been completed and it's been triggered.
900 return ! $this->is_wizard_completed() && get_option( "{$this->prefix}_show_wizard", false );
901 }
902
903 /**
904 * Trigger the wizard to be shown.
905 */
906 public function trigger_wizard() {
907 update_option( "{$this->prefix}_show_wizard", true );
908 }
909 }
910