PluginProbe
WebberZone Top 10 — Popular Posts / trunk
WebberZone Top 10 — Popular Posts vtrunk
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 1.6.2 All 116 releases
top-10 / includes / admin / settings / class-settings-wizard-api.php

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

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