diff --git a/assets/js/login-ajax.js b/assets/js/login-ajax.js new file mode 100644 index 00000000..64e7598c --- /dev/null +++ b/assets/js/login-ajax.js @@ -0,0 +1,71 @@ +(function($) { + 'use strict'; + + /** + * AJAX Login Form Handler + * + * This script converts the standard WordPress login form to use AJAX + * to bypass caching issues and provide better feedback to users. + */ + $(document).ready(function() { + + // Only run on the login form + if (!$('.wu-login-form').length) { + return; + } + + // Add a class to identify our form + $('.wu-login-form form').addClass('wu-ajax-login-form'); + + // Handle form submission + $(document).on('submit', '.wu-ajax-login-form', function(e) { + e.preventDefault(); + + var $form = $(this); + var $submitButton = $form.find('input[type="submit"]'); + var $errorContainer = $(''); + + // Remove any existing error messages + $('.wu-login-error').remove(); + + // Add the error container + $form.prepend($errorContainer); + + // Disable the submit button and show loading state + $submitButton.prop('disabled', true).val('Logging in...'); + + // Get form data + var formData = $form.serialize(); + + // Add a nonce for security + formData += '&security=' + wu_login_ajax.nonce; + + // Send the AJAX request + $.ajax({ + type: 'POST', + dataType: 'json', + url: wu_login_ajax.ajax_url, + data: { + action: 'wu_ajax_login', + data: formData + }, + success: function(response) { + if (response.success) { + // Login successful - redirect + window.location.href = response.data.redirect; + } else { + // Show error message + $errorContainer.html(response.data.message).slideDown(); + $submitButton.prop('disabled', false).val('Log In'); + } + }, + error: function() { + // Show generic error message + $errorContainer.html('An error occurred. Please try again.').slideDown(); + $submitButton.prop('disabled', false).val('Log In'); + } + }); + }); + }); + +})(jQuery); diff --git a/assets/js/login-ajax.min.js b/assets/js/login-ajax.min.js new file mode 100644 index 00000000..98ffa341 --- /dev/null +++ b/assets/js/login-ajax.min.js @@ -0,0 +1 @@ +!function(n){"use strict";n(document).ready(function(){n(".wu-login-form").length&&(n(".wu-login-form form").addClass("wu-ajax-login-form"),n(document).on("submit",".wu-ajax-login-form",function(e){e.preventDefault();var o=n(this),i=o.find('input[type="submit"]'),r=n('');n(".wu-login-error").remove(),o.prepend(r),i.prop("disabled",!0).val("Logging in...");var a=o.serialize();a+="&security="+wu_login_ajax.nonce,n.ajax({type:"POST",dataType:"json",url:wu_login_ajax.ajax_url,data:{action:"wu_ajax_login",data:a},success:function(n){n.success?window.location.href=n.data.redirect:(r.html(n.data.message).slideDown(),i.prop("disabled",!1).val("Log In"))},error:function(){r.html("An error occurred. Please try again.").slideDown(),i.prop("disabled",!1).val("Log In")}})}))})}(jQuery); diff --git a/docs/emergency-login.md b/docs/emergency-login.md new file mode 100644 index 00000000..8b3918e2 --- /dev/null +++ b/docs/emergency-login.md @@ -0,0 +1,54 @@ +# Emergency Login Feature + +The Emergency Login feature provides a way to access the WordPress login page directly, bypassing any custom login page redirections or Single Sign-On (SSO) mechanisms. This is particularly useful for administrators who need to access the WordPress admin area when the custom login page is not working properly. + +## How to Use + +To use the Emergency Login feature, add the `emergency_login=1` parameter to your WordPress login URL: + +``` +https://example.com/wp-login.php?emergency_login=1 +``` + +This will bypass: +- Redirections to the custom login page +- SSO authentication flows +- Caching mechanisms that might interfere with login + +## When to Use + +Use the Emergency Login feature in the following situations: + +1. When you're unable to log in through the custom login page +2. When you need to troubleshoot login issues +3. When you're locked out of your site due to redirection loops +4. When caching plugins are interfering with the login process + +## Security Considerations + +The Emergency Login feature is designed for administrators and site owners. While it doesn't bypass WordPress authentication (you still need valid credentials), it does bypass custom login page redirections. + +Consider implementing additional security measures: + +1. Use strong passwords for administrator accounts +2. Implement two-factor authentication +3. Limit login attempts +4. Monitor login activity + +## Troubleshooting + +If you're experiencing login issues even with the Emergency Login feature: + +1. Clear your browser cache and cookies +2. Try using a different browser +3. Disable caching plugins temporarily +4. Check for JavaScript errors in your browser console + +## Related Settings + +The Emergency Login feature works alongside the following WP Ultimo settings: + +- **Custom Login Page**: Found under Settings > Login & Registration +- **Obfuscate Original Login URL**: Found under Settings > Login & Registration + +Even when these settings are enabled, the Emergency Login parameter will allow direct access to the WordPress login page. diff --git a/inc/checkout/class-checkout-pages.php b/inc/checkout/class-checkout-pages.php index a9423042..63a3eff4 100644 --- a/inc/checkout/class-checkout-pages.php +++ b/inc/checkout/class-checkout-pages.php @@ -449,6 +449,11 @@ public function maybe_obfuscate_login_url(): void { return; } + // Emergency login bypass - allows direct access to wp-login.php + if (wu_request('emergency_login')) { + return; + } + $new_login_url = $this->get_page_url('login'); if ( ! $new_login_url) { diff --git a/inc/class-ajax.php b/inc/class-ajax.php index 3835f9aa..dcaf4f23 100644 --- a/inc/class-ajax.php +++ b/inc/class-ajax.php @@ -41,6 +41,11 @@ public function __construct() { * Load search endpoints. */ add_action('wp_ajax_wu_list_table_fetch_ajax_results', [$this, 'refresh_list_table']); + + /* + * AJAX Login handler + */ + add_action('wp_ajax_nopriv_wu_ajax_login', [$this, 'handle_ajax_login']); } /** @@ -414,4 +419,57 @@ public function render_selectize_templates(): void { wu_get_template('ui/selectize-templates'); } } + + /** + * Handles AJAX login requests. + * + * @since 2.0.0 + * @return void + */ + public function handle_ajax_login(): void { + // Check nonce + if (!check_ajax_referer('wu-ajax-login-nonce', 'security', false)) { + wp_send_json_error(array( + 'message' => __('Security check failed. Please refresh the page and try again.', 'wp-multisite-waas') + )); + } + + // Parse the form data + $form_data = array(); + parse_str($_POST['data'], $form_data); + + // Check for required fields + if (empty($form_data['log']) || empty($form_data['pwd'])) { + wp_send_json_error(array( + 'message' => __('Username and password are required.', 'wp-multisite-waas') + )); + } + + // Prevent caching + wu_no_cache(); + + // Attempt to log the user in + $credentials = array( + 'user_login' => $form_data['log'], + 'user_password' => $form_data['pwd'], + 'remember' => isset($form_data['rememberme']) + ); + + $user = wp_signon($credentials, is_ssl()); + + // Check for errors + if (is_wp_error($user)) { + wp_send_json_error(array( + 'message' => $user->get_error_message() + )); + } + + // Determine redirect URL + $redirect_to = !empty($form_data['redirect_to']) ? $form_data['redirect_to'] : admin_url(); + + // Success response + wp_send_json_success(array( + 'redirect' => $redirect_to + )); + } } diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index 315c4a3b..60ed0f8f 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -412,6 +412,7 @@ public function load_public_apis(): void { require_once wu_path('inc/functions/generator.php'); require_once wu_path('inc/functions/color.php'); require_once wu_path('inc/functions/danger.php'); + require_once wu_path('inc/functions/transients.php'); /* * Admin helper functions @@ -908,6 +909,11 @@ protected function load_managers(): void { */ WP_Ultimo\Managers\Cache_Manager::get_instance(); + /* + * Loads the Transient manager. + */ + WP_Ultimo\Managers\Transient_Manager::get_instance(); + /** * Loads views overrides */ diff --git a/inc/functions/transients.php b/inc/functions/transients.php new file mode 100644 index 00000000..e3911469 --- /dev/null +++ b/inc/functions/transients.php @@ -0,0 +1,95 @@ +set_transient($transient_name, $value, $expiration, $group); +} + +/** + * Gets a transient value. + * + * @since 2.0.0 + * @param string $transient_name The name of the transient. + * @return mixed The value of the transient or false if it doesn't exist. + */ +function wu_get_transient($transient_name) { + + return \WP_Ultimo\Managers\Transient_Manager::get_instance()->get_transient($transient_name); +} + +/** + * Deletes a transient. + * + * @since 2.0.0 + * @param string $transient_name The name of the transient. + * @return bool True if the transient was deleted, false otherwise. + */ +function wu_delete_transient($transient_name) { + + return \WP_Ultimo\Managers\Transient_Manager::get_instance()->delete_transient($transient_name); +} + +/** + * Deletes all transients in a group. + * + * @since 2.0.0 + * @param string $group The group of transients to delete. + * @return bool True if all transients were deleted, false otherwise. + */ +function wu_delete_transients_by_group($group) { + + return \WP_Ultimo\Managers\Transient_Manager::get_instance()->delete_transients_by_group($group); +} + +/** + * Deletes all transients. + * + * @since 2.0.0 + * @return bool True if all transients were deleted, false otherwise. + */ +function wu_delete_transients() { + + return \WP_Ultimo\Managers\Transient_Manager::get_instance()->delete_transients(); +} + +/** + * Gets all registered transients. + * + * @since 2.0.0 + * @return array The list of registered transients. + */ +function wu_get_registered_transients() { + + return \WP_Ultimo\Managers\Transient_Manager::get_instance()->get_registered_transients(); +} + +/** + * Gets all registered transients in a group. + * + * @since 2.0.0 + * @param string $group The group of transients to get. + * @return array The list of registered transients in the group. + */ +function wu_get_registered_transients_by_group($group) { + + return \WP_Ultimo\Managers\Transient_Manager::get_instance()->get_registered_transients_by_group($group); +} diff --git a/inc/managers/class-transient-manager.php b/inc/managers/class-transient-manager.php new file mode 100644 index 00000000..508d67df --- /dev/null +++ b/inc/managers/class-transient-manager.php @@ -0,0 +1,210 @@ +transients[$group])) { + $this->transients[$group] = array(); + } + + if (!in_array($transient_name, $this->transients[$group])) { + $this->transients[$group][] = $transient_name; + } + } + + /** + * Sets a transient and registers it with the manager. + * + * @since 2.0.0 + * @param string $transient_name The name of the transient. + * @param mixed $value The value to store. + * @param int $expiration Optional. Time until expiration in seconds. Default 0 (no expiration). + * @param string $group Optional. The group the transient belongs to. Default 'general'. + * @return bool True if the transient was set, false otherwise. + */ + public function set_transient($transient_name, $value, $expiration = 0, $group = 'general') { + + // Register the transient + $this->register_transient($transient_name, $group); + + // Set the transient + return set_site_transient($transient_name, $value, $expiration); + } + + /** + * Gets a transient value. + * + * @since 2.0.0 + * @param string $transient_name The name of the transient. + * @return mixed The value of the transient or false if it doesn't exist. + */ + public function get_transient($transient_name) { + + return get_site_transient($transient_name); + } + + /** + * Deletes a transient. + * + * @since 2.0.0 + * @param string $transient_name The name of the transient. + * @return bool True if the transient was deleted, false otherwise. + */ + public function delete_transient($transient_name) { + + return delete_site_transient($transient_name); + } + + /** + * Deletes all transients in a group. + * + * @since 2.0.0 + * @param string $group The group of transients to delete. + * @return bool True if all transients were deleted, false otherwise. + */ + public function delete_transients_by_group($group) { + + if (!isset($this->transients[$group])) { + return false; + } + + $success = true; + + foreach ($this->transients[$group] as $transient_name) { + $result = $this->delete_transient($transient_name); + $success = $success && $result; + } + + return $success; + } + + /** + * Deletes all transients. + * + * @since 2.0.0 + * @return bool True if all transients were deleted, false otherwise. + */ + public function delete_transients() { + + $success = true; + + foreach ($this->transients as $group => $transients) { + $result = $this->delete_transients_by_group($group); + $success = $success && $result; + } + + return $success; + } + + /** + * Gets all registered transients. + * + * @since 2.0.0 + * @return array The list of registered transients. + */ + public function get_registered_transients() { + + return $this->transients; + } + + /** + * Gets all registered transients in a group. + * + * @since 2.0.0 + * @param string $group The group of transients to get. + * @return array The list of registered transients in the group. + */ + public function get_registered_transients_by_group($group) { + + if (!isset($this->transients[$group])) { + return array(); + } + + return $this->transients[$group]; + } + + /** + * Registers our transients with WordPress for cleanup. + * + * @since 2.0.0 + * @param bool $external_object_cache Whether an external object cache is being used. + * @return bool The original value of $external_object_cache. + */ + public function register_transients_for_cleanup($external_object_cache) { + + // Only run this once + static $ran = false; + + if ($ran) { + return $external_object_cache; + } + + $ran = true; + + // Register our transients with WordPress + foreach ($this->transients as $group => $transients) { + foreach ($transients as $transient_name) { + wp_cache_add_non_persistent_groups('site-transient_' . $transient_name); + } + } + + return $external_object_cache; + } + +} diff --git a/inc/sso/class-sso.php b/inc/sso/class-sso.php index a0f907e8..9b7ab33f 100644 --- a/inc/sso/class-sso.php +++ b/inc/sso/class-sso.php @@ -321,6 +321,11 @@ public function handle_auth_redirect() { global $pagenow; + // Emergency login bypass - allows direct access to wp-login.php + if (wu_request('emergency_login')) { + return true; + } + $broker = $this->get_broker(); if ( ! $broker) { diff --git a/inc/ui/class-login-form-element.php b/inc/ui/class-login-form-element.php index 27de41db..5b86e167 100644 --- a/inc/ui/class-login-form-element.php +++ b/inc/ui/class-login-form-element.php @@ -301,6 +301,17 @@ public function fields() { public function register_scripts(): void { wp_enqueue_style('wu-admin'); + + // Register and enqueue the AJAX login script + wp_register_script('wu-login-ajax', wu_get_asset('login-ajax.min.js', 'js'), array('jquery'), WP_Ultimo::VERSION, true); + + // Localize the script with necessary data + wp_localize_script('wu-login-ajax', 'wu_login_ajax', array( + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('wu-ajax-login-nonce'), + )); + + wp_enqueue_script('wu-login-ajax'); } /** diff --git a/views/dashboard-widgets/login-form.php b/views/dashboard-widgets/login-form.php index 8e459d2b..77fa3ae2 100644 --- a/views/dashboard-widgets/login-form.php +++ b/views/dashboard-widgets/login-form.php @@ -5,7 +5,7 @@ * @since 2.0.0 */ ?> -
+