Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions inc/auth/class-passwordless-auth-manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ public function init() {
$this->passkeys = new Passkey_Service();
$this->otp = new Email_OTP_Service();

if ( ! $this->is_enabled()) {
return;
}

add_action('init', [$this, 'register_assets']);
add_action('init', [$this, 'maybe_install_tables'], 5);

Expand Down Expand Up @@ -79,6 +83,17 @@ public function otp() {
return $this->otp;
}

/**
* Checks if passwordless login is enabled.
*
* @since 2.13.2
* @return bool
*/
public function is_enabled() {

return (bool) wu_get_setting('use_passwordless_login', 0);
}

/**
* Registers AJAX hooks on both WordPress AJAX and Ultimate Multisite light AJAX.
*
Expand Down Expand Up @@ -189,6 +204,10 @@ public function register_assets() {
*/
public function enqueue_assets() {

if ( ! $this->is_enabled()) {
return;
}

if ( ! wp_script_is('wu-passwordless-auth', 'registered')) {
$this->register_assets();
}
Expand Down Expand Up @@ -225,6 +244,10 @@ public function enqueue_assets() {
*/
public function enqueue_login_assets() {

if ( ! $this->is_enabled()) {
return;
}

$this->enqueue_assets();

if ($this->is_password_fallback()) {
Expand All @@ -245,6 +268,10 @@ public function enqueue_login_assets() {
*/
public function render_wp_login_form() {

if ( ! $this->is_enabled()) {
return;
}

if ($this->is_password_fallback()) {
return;
}
Expand Down Expand Up @@ -273,6 +300,10 @@ public function render_wp_login_form() {
*/
public function get_login_form_markup($args = []) {

if ( ! $this->is_enabled()) {
return '';
}

$args = wp_parse_args(
$args,
[
Expand Down Expand Up @@ -402,6 +433,7 @@ public function get_inline_login_markup($field_type) {
public function ajax_start() {

$this->verify_ajax_request();
$this->ensure_enabled_for_ajax();

$identifier = sanitize_text_field(wu_request('identifier'));
$user = $this->find_user($identifier);
Expand Down Expand Up @@ -431,6 +463,7 @@ public function ajax_start() {
public function ajax_verify_otp() {

$this->verify_ajax_request();
$this->ensure_enabled_for_ajax();

$user = $this->otp->verify(wu_request('token'), wu_request('code'));

Expand Down Expand Up @@ -466,6 +499,7 @@ public function ajax_verify_otp() {
public function ajax_verify_passkey() {

$this->verify_ajax_request();
$this->ensure_enabled_for_ajax();

$credential = json_decode($this->get_json_request_value('credential'), true);

Expand Down Expand Up @@ -496,6 +530,7 @@ public function ajax_verify_passkey() {
public function ajax_register_options() {

$this->verify_ajax_request();
$this->ensure_enabled_for_ajax();

if ( ! is_user_logged_in()) {
$this->send_error(new \WP_Error('not_logged_in', __('You need to be logged in to create a passkey.', 'ultimate-multisite')));
Expand All @@ -517,6 +552,7 @@ public function ajax_register_options() {
public function ajax_register_verify() {

$this->verify_ajax_request();
$this->ensure_enabled_for_ajax();

if ( ! is_user_logged_in()) {
$this->send_error(new \WP_Error('not_logged_in', __('You need to be logged in to create a passkey.', 'ultimate-multisite')));
Expand Down Expand Up @@ -552,6 +588,21 @@ protected function verify_ajax_request() {
check_ajax_referer('wu_passwordless_auth', 'nonce');
}

/**
* Sends an error response when passwordless login is disabled.
*
* @since 2.13.2
* @return void
*/
protected function ensure_enabled_for_ajax() {

if ($this->is_enabled()) {
return;
}

$this->send_error(new \WP_Error('passwordless_login_disabled', __('Passwordless login is disabled.', 'ultimate-multisite')));
}

/**
* Finds a user by email or username.
*
Expand Down
11 changes: 9 additions & 2 deletions inc/checkout/class-checkout.php
Original file line number Diff line number Diff line change
Expand Up @@ -3280,9 +3280,16 @@ public function register_scripts(): void {
// Enqueue password styles (includes dashicons as dependency).
wp_enqueue_style('wu-password');

\WP_Ultimo\Auth\Passwordless_Auth_Manager::get_instance()->enqueue_assets();
$script_dependencies = ['jquery-core', 'wu-vue', 'moment', 'wu-block-ui', 'wu-functions', 'password-strength-meter', 'wu-password-strength', 'underscore', 'wp-polyfill', 'wp-hooks', 'wu-cookie-helpers', 'wu-password-toggle'];

wp_register_script('wu-checkout', wu_get_asset('checkout.js', 'js'), ['jquery-core', 'wu-vue', 'moment', 'wu-block-ui', 'wu-functions', 'password-strength-meter', 'wu-password-strength', 'underscore', 'wp-polyfill', 'wp-hooks', 'wu-cookie-helpers', 'wu-password-toggle', 'wu-passwordless-auth'], wu_get_version(), true);
$passwordless_auth = \WP_Ultimo\Auth\Passwordless_Auth_Manager::get_instance();

if ($passwordless_auth->is_enabled()) {
$passwordless_auth->enqueue_assets();
$script_dependencies[] = 'wu-passwordless-auth';
}

wp_register_script('wu-checkout', wu_get_asset('checkout.js', 'js'), $script_dependencies, wu_get_version(), true);

wp_set_script_translations('wu-password-toggle', 'ultimate-multisite');

Expand Down
12 changes: 12 additions & 0 deletions inc/class-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,17 @@ public function default_sections(): void {
]
);

$this->add_field(
'login-and-registration',
'use_passwordless_login',
[
'title' => __('Use Passwordless Login', 'ultimate-multisite'),
'desc' => __('When enabled, login forms ask for an email address and let customers sign in with a passkey or a one-time email code. Keep this disabled to use regular username and password login.', 'ultimate-multisite'),
'type' => 'toggle',
'default' => 0,
]
);

$this->add_field(
'login-and-registration',
'default_login_page',
Expand Down Expand Up @@ -2100,6 +2111,7 @@ public static function get_setting_defaults(): array {
'enable_registration' => 1,
'enable_email_verification' => 'free_only',
'enable_custom_login_page' => 0,
'use_passwordless_login' => 0,
'default_login_page' => 0,
'obfuscate_original_login_url' => 0,
'subsite_custom_login_logo' => 0,
Expand Down
87 changes: 71 additions & 16 deletions inc/ui/class-login-form-element.php
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,11 @@ public function register_scripts() {

wp_set_script_translations('wu-password-toggle', 'ultimate-multisite');

\WP_Ultimo\Auth\Passwordless_Auth_Manager::get_instance()->enqueue_assets();
$passwordless_auth = \WP_Ultimo\Auth\Passwordless_Auth_Manager::get_instance();

if ($passwordless_auth->is_enabled()) {
$passwordless_auth->enqueue_assets();
}

// Enqueue password strength scripts for reset password page.
if ($this->is_reset_password_page()) {
Expand Down Expand Up @@ -802,21 +806,72 @@ public function output($atts, $content = null) {
$redirect_to = $atts['main_redirect_path'];
}

$fields = [
'passwordless_login' => [
'type' => 'html',
'content' => \WP_Ultimo\Auth\Passwordless_Auth_Manager::get_instance()->get_login_form_markup(
[
'context' => 'login-form',
'redirect_to' => $redirect_to,
'redirect_type' => $atts['redirect_type'],
'fallback_url' => $this->get_passwordless_fallback_url($redirect_to),
]
),
'classes' => '',
'wrapper_classes' => 'wu-w-full wu-bg-none',
],
];
$passwordless_auth = \WP_Ultimo\Auth\Passwordless_Auth_Manager::get_instance();

if ($passwordless_auth->is_enabled()) {
$fields = [
'passwordless_login' => [
'type' => 'html',
'content' => $passwordless_auth->get_login_form_markup(
[
'context' => 'login-form',
'redirect_to' => $redirect_to,
'redirect_type' => $atts['redirect_type'],
'fallback_url' => $this->get_passwordless_fallback_url($redirect_to),
]
),
'classes' => '',
'wrapper_classes' => 'wu-w-full wu-bg-none',
],
];
} else {
$fields = [
'log' => [
'type' => 'text',
'title' => $atts['label_username'],
'placeholder' => $atts['placeholder_username'],
'tooltip' => '',
'html_attr' => [
'autocomplete' => 'username',
],
],
'pwd' => [
'type' => 'password',
'title' => $atts['label_password'],
'placeholder' => $atts['placeholder_password'],
'tooltip' => '',
'html_attr' => [
'autocomplete' => 'current-password',
],
],
];

if ($atts['remember']) {
$fields['rememberme'] = [
'type' => 'toggle',
'title' => $atts['label_remember'],
'desc' => $atts['desc_remember'],
];
}

$fields['redirect_to'] = [
'type' => 'hidden',
'value' => $redirect_to,
];

$fields['wu_login_form_redirect_type'] = [
'type' => 'hidden',
'value' => $atts['redirect_type'],
];

$fields['wp-submit'] = [
'type' => 'submit',
'title' => $atts['label_log_in'],
'value' => $atts['label_log_in'],
'classes' => 'button button-primary wu-w-full',
'wrapper_classes' => 'wu-items-end wu-bg-none',
];
}

/*
* Use wp_lostpassword_url() so the lostpassword_url filter keeps
Expand Down
23 changes: 23 additions & 0 deletions tests/WP_Ultimo/Auth/Passwordless_Auth_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public function set_up() {

parent::set_up();

wu_save_setting('use_passwordless_login', 1);

$this->install_auth_tables();
$this->truncate_auth_tables();
}
Expand All @@ -40,6 +42,7 @@ public function tear_down() {
remove_all_filters('wu_passwordless_should_send_otp');

$this->truncate_auth_tables();
wu_save_setting('use_passwordless_login', 0);

parent::tear_down();
}
Expand Down Expand Up @@ -320,6 +323,26 @@ function () {
unset($_POST['nonce'], $_POST['identifier'], $_REQUEST['nonce'], $_REQUEST['identifier']);
}

/**
* Tests passwordless AJAX endpoints fail closed when the setting is disabled.
*/
public function test_ajax_start_fails_when_passwordless_login_is_disabled() {

wu_save_setting('use_passwordless_login', 0);

$nonce = wp_create_nonce('wu_passwordless_auth');

$_POST['nonce'] = $nonce;
$_REQUEST['nonce'] = $nonce;

$response = $this->capture_ajax_json([Passwordless_Auth_Manager::get_instance(), 'ajax_start']);

$this->assertFalse($response['success']);
$this->assertSame('passwordless_login_disabled', $response['data']['code']);

unset($_POST['nonce'], $_REQUEST['nonce']);
}

/**
* Sets a protected property for dependency injection in tests.
*
Expand Down
15 changes: 14 additions & 1 deletion tests/WP_Ultimo/Settings_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ public function test_get_sections_contains_default_sections() {
}

public function test_get_sections_caches_result() {
$first = $this->settings->get_sections();
$first = $this->settings->get_sections();
$second = $this->settings->get_sections();
$this->assertSame($first, $second);
}
Expand Down Expand Up @@ -367,6 +367,12 @@ public function test_get_setting_defaults_returns_array() {
$this->assertIsArray($defaults);
}

public function test_get_setting_defaults_disables_passwordless_login() {
$defaults = Settings::get_setting_defaults();
$this->assertArrayHasKey('use_passwordless_login', $defaults);
$this->assertSame(0, $defaults['use_passwordless_login']);
}

// ------------------------------------------------------------------
// General section fields
// ------------------------------------------------------------------
Expand Down Expand Up @@ -405,6 +411,13 @@ public function test_login_section_has_enable_registration_field() {
$this->assertArrayHasKey('enable_registration', $section['fields']);
}

public function test_login_section_has_use_passwordless_login_field() {
$section = $this->settings->get_section('login-and-registration');
$this->assertArrayHasKey('use_passwordless_login', $section['fields']);
$this->assertSame('toggle', $section['fields']['use_passwordless_login']['type']);
$this->assertSame(0, $section['fields']['use_passwordless_login']['default']);
}

public function test_login_section_has_default_role_field() {
$section = $this->settings->get_section('login-and-registration');
$this->assertArrayHasKey('default_role', $section['fields']);
Expand Down
Loading
Loading