<?php
/**
* LearnDash class for displaying the setup wizard.
*
* @package LearnDash
* @since 4.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'LearnDash_Setup_Wizard' ) ) {
/**
* Setup wizard class.
*/
class LearnDash_Setup_Wizard {
/**
* The opened status, can be completed, ongoing or closed.
*/
const STATUS_KEY = 'learndash_setup_wizard_status';
const STATUS_COMPLETED = 'completed';
const STATUS_ONGOING = 'ongoing';
const STATUS_CLOSED = 'closed';
const DATA_KEY = 'learndash_setup_wizard';
const CERTIFICATE_BUILDER_SLUG = 'learndash-certificate-builder/learndash-certificate-builder.php';
const COURSE_GRID_SLUG = 'learndash-course-grid/learndash_course_grid.php';
const WOOCOMMERCE_SLUG = 'woocommerce/woocommerce.php';
const LEARNDASH_WOOCOMMERCE_SLUG = 'learndash-woocommerce/learndash_woocommerce.php';
const HANDLE = 'learndash-setup-wizard';
const LICENSE_KEY = 'nss_plugin_license_sfwd_lms';
const LICENSE_EMAIL_KEY = 'nss_plugin_license_email_sfwd_lms';
const ADMIN_REDIRECT_PAGE = 'admin.php?page=learndash-setup';
const FINAL_ADMIN_REDIRECT_PAGE = 'admin.php?page=learndash-setup';
/**
* The single instance of the class.
*/
public function __construct() {
if ( ! is_admin() ) {
return;
}
add_action( 'learndash_activated', array( $this, 'set_redirect_flag' ) );
add_action( 'admin_init', array( $this, 'redirect_after_activation' ), 1 );
add_action( 'admin_menu', array( $this, 'register_menu' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
add_action( 'admin_init', array( $this, 'dismiss' ) );
add_action( 'wp_ajax_learndash_setup_wizard_verify_license', array( $this, 'verify_license' ) );
add_action( 'wp_ajax_learndash_setup_wizard_save_data', array( $this, 'save_data' ) );
add_action( 'wp_ajax_learndash_finalize', array( $this, 'finalize_setup' ) );
add_action( 'admin_post_stripe_connect_wizard_process', array( $this, 'enable_stripe_connect_and_redirect' ) );
}
/**
* Enable stripe connect and then redirect to the wizard again.
*/
public function enable_stripe_connect_and_redirect() {
// enable stripe connect.
if ( LearnDash_Settings_Section_Stripe_Connect::is_stripe_connected() ) {
LearnDash_Settings_Section::set_section_setting( 'LearnDash_Settings_Section_Stripe_Connect', 'enabled', 'yes' );
}
// force update wizard data.
$this->update_data( 'charge', 'yes' );
$this->update_data( 'charge_method', 'stripe' );
learndash_safe_redirect( admin_url( 'admin.php?page=' . self::HANDLE ) );
}
/**
* Check when we need to show the wizard and set an option for that.
*
* @since 4.5.0
*
* @return void
*/
public function set_redirect_flag() {
if ( ! $this->should_display() ) {
return;
}
update_option( 'learndash_setup_wizard_redirect', true );
}
/**
* Redirect to the setup wizard after an activation.
*/
public function redirect_after_activation() {
$should_redirect = get_option( 'learndash_setup_wizard_redirect' );
if ( ! $should_redirect ) {
return;
}
delete_option( 'learndash_setup_wizard_redirect' );
learndash_safe_redirect( admin_url( 'admin.php?page=' . self::HANDLE ) );
}
/**
* Dismiss
*/
public function dismiss() {
if ( ! isset( $_GET['page'] ) || ! isset( $_GET['dismiss'] ) || ! isset( $_GET['nonce'] ) ) {
return;
}
if ( self::HANDLE !== sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) {
return;
}
$nonce = sanitize_text_field( wp_unslash( $_GET['nonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'ld_setup_wizard_dismiss' ) ) {
return;
}
update_option( self::STATUS_KEY, self::STATUS_CLOSED );
learndash_safe_redirect( admin_url( self::ADMIN_REDIRECT_PAGE ) );
}
/**
* Returns the redirect URL after the wizard is completed.
*
* @since 4.2.0
*
* @return string The redirect URL.
*/
private function get_completed_redirect_url(): string {
/**
* Filter the URL to redirect to after the setup wizard is completed.
*
* @since 4.1.2
*
* @param string $url The URL to redirect to.
*/
return apply_filters( 'learndash_setup_wizard_completed_redirect_url', admin_url( self::FINAL_ADMIN_REDIRECT_PAGE ) );
}
/**
* Ajax endpoint for finalize the setup.
*/
public function finalize_setup() {
$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
if ( empty( $nonce ) ) {
wp_send_json_error();
}
if ( ! wp_verify_nonce( $nonce, 'ld_setup_wizard_finalize' ) ) {
wp_send_json_error();
}
/**
* React data.
*
* @var array $data
*/
$data = isset( $_POST['data'] ) ? wp_unslash( $_POST['data'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$done = false;
switch ( $data['step'] ) {
case 'save_default_currency':
LearnDash_Settings_Section::set_section_settings_all(
'LearnDash_Settings_Section_Payments_Defaults',
array(
'country' => $data['currency_country'],
'currency' => $data['currency'],
)
);
break;
case 'create_registration_pages':
// enable anyone can register option and create registration pages.
update_option( 'users_can_register', true );
$this->create_registration_pages();
$this->create_profile_page();
break;
case 'process_course_listing':
// Import demo course.
// phpcs:ignore Generic.CodeAnalysis.EmptyStatement -- TODO: Remove this comment later.
if ( 'yes' === $data['course_demo'] ) {
Learndash_Admin_Import_Export::import_demo_content();
}
// Create course listing page.
if ( 'multiple' === $data['courses_amount'] ) {
$this->create_courses_listing_page();
}
// install course grid plugin.
if ( 'true' === $data['course_grid'] ) {
$ret = $this->maybe_install_a_plugin( self::COURSE_GRID_SLUG );
if ( true === $ret ) {
activate_plugin( self::COURSE_GRID_SLUG );
}
}
break;
case 'process_certificate_builder':
// install certificate builder plugin.
if ( 'true' === $data['certificate_builder'] ) {
$ret = $this->maybe_install_a_plugin( self::CERTIFICATE_BUILDER_SLUG );
if ( true === $ret ) {
activate_plugin( self::CERTIFICATE_BUILDER_SLUG );
}
}
break;
case 'process_woo':
// install woocommerce plugin and learndash add-on.
if ( 'true' === $data['woocommerce'] ) {
$ret = $this->maybe_install_a_plugin( self::WOOCOMMERCE_SLUG );
if ( true === $ret ) {
activate_plugin( self::WOOCOMMERCE_SLUG );
}
$ret = $this->maybe_install_a_plugin( self::LEARNDASH_WOOCOMMERCE_SLUG );
if ( true === $ret ) {
activate_plugin( self::LEARNDASH_WOOCOMMERCE_SLUG );
}
}
break;
case 'update_settings':
// user from email address.
$from_email = 'yes' === $data['use_registered_email'] ? $data['email'] : filter_var( $data['notification_email'], FILTER_VALIDATE_EMAIL );
LearnDash_Settings_Section::set_section_setting( 'LearnDash_Settings_Section_Emails_Sender_Settings', 'from_email', $from_email );
// group access and management.
if ( is_array( $data['course_type'] ) && in_array( 'group_courses', $data['course_type'], true ) ) {
// optional group settings.
LearnDash_Settings_Section::set_section_setting( 'LearnDash_Settings_Groups_CPT', 'public', isset( $data['public_group'] ) && $data['public_group'] ? 'yes' : '' );
LearnDash_Settings_Section::set_section_setting( 'LearnDash_Settings_Groups_CPT', 'has_archive', isset( $data['group_archive_page'] ) && $data['group_archive_page'] ? 'yes' : '' );
LearnDash_Settings_Section::set_section_setting( 'LearnDash_Settings_Section_Groups_Group_Leader_User', 'manage_courses_enabled', isset( $data['manage_user'] ) && $data['manage_user'] ? 'yes' : '' );
LearnDash_Settings_Section::set_section_setting( 'LearnDash_Settings_Section_Groups_Group_Leader_User', 'groups_autoenroll_managed', isset( $data['group_auto_enroll'] ) && $data['group_auto_enroll'] ? 'yes' : '' );
LearnDash_Settings_Section::set_section_setting( 'LearnDash_Settings_Section_Groups_Group_Leader_User', 'courses_autoenroll', isset( $data['course_auto_enroll'] ) && $data['course_auto_enroll'] ? 'yes' : '' );
LearnDash_Settings_Section::set_section_setting( 'LearnDash_Settings_Section_Groups_Group_Leader_User', 'bypass_course_limits', isset( $data['bypass_course_limit'] ) && $data['bypass_course_limit'] ? 'yes' : '' );
}
update_option( self::STATUS_KEY, self::STATUS_COMPLETED );
$done = true;
break;
}
// preparing return data.
$result_data = array(
'completed' => $done,
);
if ( $done ) {
$result_data['redirect'] = $this->get_completed_redirect_url();
/**
* Action to be run after the setup wizard is completed.
*
* @since 4.1.2
*/
do_action( 'learndash_setup_wizard_completed' );
}
wp_send_json_success( $result_data );
}
/**
* Install a plugin
*
* @param string $slug The plugin slug.
*
* @return bool
*/
protected function maybe_install_a_plugin( string $slug ) {
$plugins = get_plugins();
if ( isset( $plugins[ $slug ] ) && is_plugin_inactive( $slug ) ) {
return true; // plugin is installed but not activated.
}
if ( ! function_exists( 'plugins_api' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
}
$slug = dirname( $slug );
$api = plugins_api(
'plugin_information',
array(
'slug' => $slug,
)
);
if ( is_wp_error( $api ) ) {
WP_DEBUG && error_log( $api->get_error_message() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return false;
}
$status = install_plugin_install_status( $api );
if ( 'install' === $status['status'] ) {
return $this->install( $slug );
}
return false;
}
/**
* Install a plugin
*
* @param string $slug Plugin slug.
*
* @return bool
*/
public function install( string $slug ) {
// prepare for install.
include_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
include_once ABSPATH . 'wp-admin/includes/plugin-install.php';
include_once ABSPATH . 'wp-admin/includes/plugin.php';
include_once ABSPATH . 'wp-admin/includes/file.php';
$skin = new WP_Ajax_Upgrader_Skin();
/**
* Response object.
*
* @var object api
*/
$api = plugins_api(
'plugin_information',
array(
'slug' => sanitize_key( $slug ),
'fields' => array( 'sections' => false ),
)
);
if ( is_wp_error( $api ) ) {
WP_DEBUG && error_log( $api->get_error_message() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return false;
}
$upgrade_er = new Plugin_Upgrader( $skin );
$result = $upgrade_er->install( isset( $api->download_link ) ? $api->download_link : $api->download_url );
if ( is_wp_error( $result ) ) {
WP_DEBUG && error_log( $result->get_error_message() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
return false;
}
return $result;
}
/**
* Create courses listing page
*
* @return void
*/
protected function create_courses_listing_page() {
wp_insert_post(
array(
'post_title' => __( 'Courses', 'learndash' ),
'post_content' => '<!-- wp:learndash/ld-course-list /-->',
'post_status' => 'publish',
'post_type' => 'page',
)
);
}
/**
* Create profile page during Setup Wizard completion.
*
* @since 4.4.1
*
* @return void
*/
protected function create_profile_page(): void {
wp_insert_post(
array(
'post_title' => __( 'Profile', 'learndash' ),
'post_content' => '<!-- wp:learndash/ld-profile /-->',
'post_status' => 'publish',
'post_type' => 'page',
)
);
}
/**
* Create registration, registration success, reset password and profile pages.
*/
protected function create_registration_pages() {
// Data for each of the pages being created.
$page_data = array(
array(
'identifier' => 'registration',
'title' => 'Registration',
'content' => '<!-- wp:learndash/ld-registration /-->',
),
array(
'identifier' => 'registration_success',
'title' => 'Registration Success',
'content' => '<!-- wp:paragraph --><p>' . __( 'Welcome', 'learndash' ) . '</p><!-- /wp:paragraph -->',
),
array(
'identifier' => 'reset_password',
'title' => 'Reset Password',
'content' => '<!-- wp:learndash/ld-reset-password {"width":""} /-->',
),
);
foreach ( $page_data as $page_values ) {
$page_id = LearnDash_Settings_Section::get_section_setting( 'LearnDash_Settings_Section_Registration_Pages', $page_values['identifier'] );
if ( ! empty( $page_id ) ) {
continue;
}
LearnDash_Settings_Section::set_section_setting(
'LearnDash_Settings_Section_Registration_Pages',
$page_values['identifier'],
wp_insert_post(
array(
'post_title' => $page_values['title'],
'post_content' => $page_values['content'],
'post_status' => 'publish',
'post_type' => 'page',
)
)
);
}
}
/**
* An ajax endpoint for saving wizard data. When the
* user move to next step, we will store the current state in the db.
*/
public function save_data() {
$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'ld_setup_wizard_save_data' ) ) {
wp_send_json_error();
}
$data = isset( $_POST['data'] )
? $this->sanitize_text_fields( (array) wp_unslash( $_POST['data'] ) ) // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
: array();
foreach ( $data as $key => $val ) {
$this->update_data( $key, $val );
}
update_option( self::STATUS_KEY, self::STATUS_ONGOING );
}
/**
* Ajax endpoint, for trigger a request to validate the license key.
*/
public function verify_license() {
$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
if ( empty( $nonce ) || ! wp_verify_nonce( $nonce, 'ld_setup_wizard_verify_license' ) ) {
wp_send_json_error();
}
$email = isset( $_POST['email'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['email'] ) ) ) : '';
$license_key = isset( $_POST['license_key'] ) ? trim( sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) ) : '';
if ( ! filter_var( $email, FILTER_VALIDATE_EMAIL ) || empty( $license_key ) ) {
wp_send_json_error();
}
update_option( self::LICENSE_KEY, $license_key );
update_option( self::LICENSE_EMAIL_KEY, $email );
// Licensing validation.
$license_status = learndash_validate_hub_license( $email, $license_key );
if ( ! $license_status ) {
wp_send_json_error();
}
// Store the data.
update_option( self::STATUS_KEY, self::STATUS_ONGOING );
wp_send_json_success();
}
/**
*
* Update the wizard data.
*
* @param string $key Data key.
* @param mixed $value Data value.
* @param string $option_name Option name. Default: 'learndash_setup_wizard'.
*/
private function update_data( string $key, $value, string $option_name = self::DATA_KEY ): void {
$data = get_option( $option_name, array() );
if ( ! is_array( $data ) ) {
$data = array();
}
$data[ $key ] = $value;
update_option( $option_name, $data );
}
/**
* Get the list of scenes.
*
* @return array The list of scenes. [scene_key => scene_data].
*/
private function get_scenes(): array {
$available_scenes = array(
'step-0' => esc_html__( 'Welcome', 'learndash' ),
'step-1' => esc_html__( 'Your Info', 'learndash' ),
'step-2' => esc_html__( 'Your Courses', 'learndash' ),
'step-3' => esc_html__( 'Payment', 'learndash' ),
'step-4' => esc_html__( 'Summary', 'learndash' ),
);
/**
* Filters the available scenes for the setup wizard.
*
* @since 4.2.0
*
* @param array $scenes The list of scenes. [scene_key => scene_description].
*/
$available_scenes = apply_filters( 'learndash_setup_wizard_available_scenes', $available_scenes );
// make sure that we have at least one scene.
if ( empty( $available_scenes ) ) {
$available_scenes = array( 'step-0' => esc_html__( 'Welcome', 'learndash' ) );
}
$scenes = array();
$keys = array_keys( $available_scenes );
$pos = 0;
foreach ( $available_scenes as $scene_key => $scene_description ) {
$scenes[ $scene_key ] = array(
'description' => $scene_description,
'next' => isset( $keys[ $pos + 1 ] ) ? $keys[ $pos + 1 ] : '',
'prev' => isset( $keys[ $pos - 1 ] ) ? $keys[ $pos - 1 ] : '',
);
$pos++;
}
return $scenes;
}
/**
* Register the script
*/
public function enqueue_scripts() {
$screen = get_current_screen();
if ( is_object( $screen ) && 'toplevel_page_learndash-setup-wizard' === $screen->id ) {
wp_register_style(
self::HANDLE,
LEARNDASH_LMS_PLUGIN_URL . 'assets/js/setup-wizard/dist/css/style.css',
array(),
constant( 'SCRIPT_DEBUG' ) === true ? time() : LEARNDASH_VERSION
);
wp_enqueue_style( self::HANDLE );
wp_register_script(
self::HANDLE,
LEARNDASH_LMS_PLUGIN_URL . 'assets/js/setup-wizard/dist/js/index.js',
array( 'react', 'react-dom', 'wp-i18n' ),
constant( 'SCRIPT_DEBUG' ) === true ? time() : LEARNDASH_VERSION,
true
);
$data = get_option( self::DATA_KEY );
if ( ! is_array( $data ) ) {
$data = [];
}
$currency_code = learndash_get_currency_code();
$currency_country = LearnDash_Settings_Section::get_section_setting( 'LearnDash_Settings_Section_Payments_Defaults', 'country' ) ?? '';
// scenes processing.
$scenes = $this->get_scenes();
$current_scene = isset( $data['scene'] ) && isset( $scenes[ $data['scene'] ] ) ? $data['scene'] : array_keys( $scenes )[0];
wp_localize_script(
self::HANDLE,
'ldSetupWizard',
array(
'urls' => array(
'assets' => LEARNDASH_LMS_PLUGIN_URL . 'assets/js/setup-wizard/dist/',
'dismiss' => admin_url(
wp_sprintf(
'admin.php?page=%s&dismiss=true&nonce=%s',
self::HANDLE,
wp_create_nonce( 'ld_setup_wizard_dismiss' )
)
),
'support' => 'https://support.learndash.com',
'stripe_connect' => LearnDash_Settings_Section_Stripe_Connect::generate_connect_url( admin_url( 'admin-post.php?action=stripe_connect_wizard_process' ) ),
'iso_4217' => 'https://en.wikipedia.org/wiki/ISO_4217#Active_codes',
'no_step_url' => $this->get_completed_redirect_url(),
),
'nonces' => array(
'verify' => wp_create_nonce( 'ld_setup_wizard_verify_license' ),
'save' => wp_create_nonce( 'ld_setup_wizard_save_data' ),
'finalize' => wp_create_nonce( 'ld_setup_wizard_finalize' ),
),
'data' => array(
'scenes' => $scenes,
'scene' => $current_scene,
'email' => $data['email'] ?? get_option( self::LICENSE_EMAIL_KEY, '' ),
'license_key' => $data['license_key'] ?? get_option( self::LICENSE_KEY, '' ),
'use_registered_email' => $data['use_registered_email'] ?? 'yes',
'notification_email' => $data['notification_email'] ?? '',
'license_validated' => $data['license_validated'] ?? 'no',
'course_demo' => $data['course_demo'] ?? 'no',
'courses_amount' => $data['courses_amount'] ?? 'single',
'course_type' => $data['course_type'] ?? array(),
'group_access' => $data['group_access'] ?? 'no',
'group_leader' => $data['group_leader'] ?? 'no',
'charge' => $data['charge'] ?? 'no',
'charge_method' => $data['charge_method'] ?? '',
'currency' => ! empty( $currency_code ) ? $currency_code : '',
'currency_country' => ! empty( $currency_country ) ? $currency_country : '',
'currency_select2_default' => ! empty( $currency_country ) ? ucwords( mb_strtolower( $currency_country ) ) . ' (' . learndash_get_currency_symbol( $currency_code ) . ') ' : '',
'stripe_connected' => LearnDash_Settings_Section_Stripe_Connect::is_stripe_connected(),
'stripe_webhook_notice' => wp_kses_post( LearnDash_Settings_Section_Stripe_Connect::get_stripe_webhook_notice() ),
),
'plugins' => array(
'certificate_builder' => is_plugin_active( self::CERTIFICATE_BUILDER_SLUG ),
'course_grid' => is_plugin_active( self::COURSE_GRID_SLUG ),
'woocommerce' => is_plugin_active( self::WOOCOMMERCE_SLUG ),
),
'currency_codes' => array(
'list' => learndash_currency_codes_list(),
),
)
);
wp_enqueue_script( self::HANDLE );
}
}
/**
* Output the html root for react app.
*/
public function render() {
?>
<div id="learndash-setup-wizard"></div>
<?php
}
/**
* Register the admin page to the tree.
*/
public function register_menu() {
add_menu_page(
__( 'Setup Wizard', 'learndash' ),
__( 'Setup Wizard', 'learndash' ),
LEARNDASH_ADMIN_CAPABILITY_CHECK,
self::HANDLE,
array( $this, 'render' )
);
// Hide the admin menu item, the page stays available.
remove_menu_page( self::HANDLE );
if (
isset( $_GET['page'] )
&& self::HANDLE === sanitize_text_field( wp_unslash( $_GET['page'] ) )
) {
// remove_menu_page() call affects the global title and makes it null in the end,
// which causes a deprecation error in WP core cause it requires it to be a string.
global $title;
$title = ''; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- It's intentional.
}
}
/**
* If there is no license key stored in the database, then we should show up the wizard
*
* @return bool
*/
protected function should_display(): bool {
$should_display = false;
$wizard_status = get_option( self::STATUS_KEY );
// The wizard is in progress, but closed by an accident or something like that.
if ( self::STATUS_ONGOING === $wizard_status ) {
$should_display = true;
}
if (
empty( get_option( self::LICENSE_KEY ) ) ||
empty( get_option( self::LICENSE_EMAIL_KEY ) )
) {
$should_display = true;
}
/**
* Filters whether the setup wizard should be displayed or not.
*
* @since 4.2.0
*
* @param bool $should_display Whether the setup wizard should be displayed or not.
*/
return apply_filters( 'learndash_setup_wizard_should_display', $should_display );
}
/**
* Sanitize an array recursively.
*
* @param array $array Array.
*
* @return array
*/
private function sanitize_text_fields( array $array ): array {
foreach ( $array as &$value ) {
$value = is_array( $value )
? $this->sanitize_text_fields( $value )
: sanitize_text_field( $value );
}
return $array;
}
}
}