Some checks are pending
CI / PHP lint (7.4) (pull_request) Waiting to run
CI / PHP lint (8.0) (pull_request) Waiting to run
CI / PHP lint (8.1) (pull_request) Waiting to run
CI / PHP lint (8.2) (pull_request) Waiting to run
CI / PHP lint (8.3) (pull_request) Waiting to run
CI / PHP lint (8.4) (pull_request) Waiting to run
CI / WordPress Coding Standards (pull_request) Waiting to run
CI / WordPress.org Plugin Check (pull_request) Waiting to run
- No border or shadow around the login box on wp-login.php (form and Microsoft block, also in button-only mode); the white area stays. - Button-only mode: the "Lost your password?" link is removed through lost_password_html_link instead of CSS only, the lostpassword, retrievepassword, rp and resetpass screens redirect to the login page and allow_password_reset refuses resets – all unless the fallback link is active. - The login stylesheet is also loaded when only the form is hidden (e.g. broken connection); before, the link and form showed there. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
362 lines
11 KiB
PHP
362 lines
11 KiB
PHP
<?php
|
|
/**
|
|
* Login page button.
|
|
*
|
|
* @package M365_Login
|
|
*/
|
|
|
|
defined( 'ABSPATH' ) || exit;
|
|
|
|
/**
|
|
* Renders the "Sign in with Microsoft" button on wp-login.php.
|
|
*/
|
|
class M365_Login_Button {
|
|
|
|
/**
|
|
* Settings.
|
|
*
|
|
* @var M365_Login_Settings
|
|
*/
|
|
private $settings;
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @param M365_Login_Settings $settings Settings.
|
|
*/
|
|
public function __construct( M365_Login_Settings $settings ) {
|
|
$this->settings = $settings;
|
|
|
|
add_action( 'login_enqueue_scripts', array( $this, 'enqueue' ) );
|
|
add_filter( 'login_body_class', array( $this, 'body_class' ) );
|
|
add_filter( 'login_message', array( $this, 'render_above' ), 20 );
|
|
add_action( 'login_footer', array( $this, 'render_below' ) );
|
|
add_shortcode( 'm365_login_button', array( $this, 'shortcode' ) );
|
|
|
|
// Custom login pages built with wp_login_form().
|
|
add_filter( 'login_form_top', array( $this, 'form_top' ), 10, 2 );
|
|
add_filter( 'login_form_bottom', array( $this, 'form_bottom' ), 10, 2 );
|
|
}
|
|
|
|
/**
|
|
* Whether the plugin should hook into wp_login_form() output.
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function inject_into_forms() {
|
|
return $this->settings->is_configured() && (bool) $this->settings->get( 'inject_form' ) && ! is_user_logged_in();
|
|
}
|
|
|
|
/**
|
|
* wp_login_form(): messages above the fields; in button-only mode the fields are wrapped and hidden.
|
|
*
|
|
* @param string $content Existing content.
|
|
* @param array $args wp_login_form() arguments.
|
|
* @return string
|
|
*/
|
|
public function form_top( $content, $args = array() ) {
|
|
if ( ! $this->inject_into_forms() ) {
|
|
return $content;
|
|
}
|
|
$this->enqueue_frontend();
|
|
$content .= $this->messages_markup();
|
|
if ( $this->password_login_hidden() ) {
|
|
$content .= '<div class="m365-login__hidden-fields" hidden>';
|
|
}
|
|
return $content;
|
|
}
|
|
|
|
/**
|
|
* wp_login_form(): closes the wrapper and appends the button.
|
|
*
|
|
* @param string $content Existing content.
|
|
* @param array $args wp_login_form() arguments.
|
|
* @return string
|
|
*/
|
|
public function form_bottom( $content, $args = array() ) {
|
|
if ( ! $this->inject_into_forms() ) {
|
|
return $content;
|
|
}
|
|
$hidden = $this->password_login_hidden();
|
|
$redirect_to = isset( $args['redirect'] ) ? (string) $args['redirect'] : '';
|
|
if ( $hidden ) {
|
|
$content .= '</div>';
|
|
}
|
|
$divider = $hidden ? '' : $this->divider_markup();
|
|
return $content . '<div class="m365-login m365-login--form">' . $divider . $this->button( $redirect_to ) . '</div>';
|
|
}
|
|
|
|
/**
|
|
* Whether password fields should be hidden right now (button-only mode without fallback).
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function password_login_hidden() {
|
|
return $this->settings->button_only() && ! M365_Login::instance()->auth->fallback_active();
|
|
}
|
|
|
|
/**
|
|
* Enqueues the stylesheet on the front end (custom login pages).
|
|
*/
|
|
public function enqueue_frontend() {
|
|
wp_enqueue_style( 'm365-login', M365_LOGIN_URL . 'assets/css/login.css', array(), M365_LOGIN_VERSION );
|
|
wp_add_inline_style( 'm365-login', $this->css_variables() );
|
|
}
|
|
|
|
/**
|
|
* Divider line markup ('' when the divider text is empty).
|
|
*
|
|
* @return string
|
|
*/
|
|
private function divider_markup() {
|
|
$divider = (string) $this->settings->get( 'divider_text' );
|
|
return '' === trim( $divider ) ? '' : '<div class="m365-login__divider" aria-hidden="true"><span>' . esc_html( $divider ) . '</span></div>';
|
|
}
|
|
|
|
/**
|
|
* Error / info messages for custom login pages.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function messages_markup() {
|
|
$html = '';
|
|
foreach ( M365_Login::instance()->auth->current_messages() as $msg ) {
|
|
$html .= '<div class="m365-login__notice m365-login__notice--' . esc_attr( $msg['type'] ) . '" role="' . ( 'error' === $msg['type'] ? 'alert' : 'status' ) . '">' . esc_html( $msg['text'] ) . '</div>';
|
|
}
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Full markup for templates and shortcodes.
|
|
*
|
|
* @param array $args {
|
|
* @type string $redirect Destination after login.
|
|
* @type bool $show_messages Show error/info messages. Default true.
|
|
* @type bool $show_divider Show the divider line. Default false.
|
|
* }
|
|
* @return string
|
|
*/
|
|
public function standalone( $args = array() ) {
|
|
if ( ! $this->settings->is_configured() || is_user_logged_in() ) {
|
|
return '';
|
|
}
|
|
$args = wp_parse_args(
|
|
$args,
|
|
array(
|
|
'redirect' => '',
|
|
'show_messages' => true,
|
|
'show_divider' => false,
|
|
)
|
|
);
|
|
$this->enqueue_frontend();
|
|
|
|
$html = '<div class="m365-login m365-login--shortcode">';
|
|
if ( $args['show_messages'] ) {
|
|
$html .= $this->messages_markup();
|
|
}
|
|
if ( $args['show_divider'] && ! $this->password_login_hidden() ) {
|
|
$html .= $this->divider_markup();
|
|
}
|
|
$html .= $this->button( esc_url_raw( (string) $args['redirect'] ) );
|
|
return $html . '</div>';
|
|
}
|
|
|
|
/**
|
|
* Whether the button should be shown for the current login screen.
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function should_render() {
|
|
if ( ! $this->settings->is_configured() || ! $this->is_login_action() ) {
|
|
return false;
|
|
}
|
|
/**
|
|
* Filters whether the Microsoft button is displayed on the login screen.
|
|
*
|
|
* @param bool $show Show the button.
|
|
*/
|
|
return (bool) apply_filters( 'm365_login_show_button', true );
|
|
}
|
|
|
|
/**
|
|
* Whether the current wp-login.php request shows the sign-in form (not interim login or another action).
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function is_login_action() {
|
|
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only routing check.
|
|
$action = isset( $_REQUEST['action'] ) ? sanitize_key( wp_unslash( $_REQUEST['action'] ) ) : 'login';
|
|
$interim = ! empty( $_REQUEST['interim-login'] );
|
|
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
|
return ! $interim && in_array( $action, array( '', 'login' ), true );
|
|
}
|
|
|
|
/**
|
|
* Whether the password form is hidden for this request.
|
|
*
|
|
* Also while the connection is broken: password sign-in is refused anyway, the fields would only mislead.
|
|
*
|
|
* @return bool
|
|
*/
|
|
private function form_hidden() {
|
|
if ( ! $this->settings->button_only() || M365_Login::instance()->auth->fallback_active() || ! $this->is_login_action() ) {
|
|
return false;
|
|
}
|
|
return $this->should_render() || ! $this->settings->is_configured();
|
|
}
|
|
|
|
/**
|
|
* Adds a body class while the password form is hidden.
|
|
*
|
|
* @param string[] $classes Body classes.
|
|
* @return string[]
|
|
*/
|
|
public function body_class( $classes ) {
|
|
if ( $this->should_render() ) {
|
|
$classes[] = 'm365-login-page';
|
|
}
|
|
if ( $this->form_hidden() ) {
|
|
$classes[] = 'm365-button-only';
|
|
}
|
|
return $classes;
|
|
}
|
|
|
|
/**
|
|
* Enqueues login styles and the small positioning script.
|
|
*/
|
|
public function enqueue() {
|
|
// Also when only the form is hidden (e.g. broken connection): the stylesheet hides the password form and links.
|
|
if ( ! $this->should_render() && ! $this->form_hidden() ) {
|
|
return;
|
|
}
|
|
wp_enqueue_style( 'm365-login', M365_LOGIN_URL . 'assets/css/login.css', array(), M365_LOGIN_VERSION );
|
|
wp_add_inline_style( 'm365-login', $this->css_variables() );
|
|
|
|
if ( 'below' === $this->settings->get( 'button_position' ) ) {
|
|
wp_enqueue_script( 'm365-login', M365_LOGIN_URL . 'assets/js/login.js', array(), M365_LOGIN_VERSION, true );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* CSS custom properties derived from the settings.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function css_variables() {
|
|
$s = $this->settings->all();
|
|
return sprintf(
|
|
'.m365-login{--m365-bg:%1$s;--m365-bg-hover:%2$s;--m365-color:%3$s;--m365-border:%4$s;--m365-radius:%5$dpx;}',
|
|
sanitize_hex_color( $s['button_bg'] ),
|
|
sanitize_hex_color( $s['button_bg_hover'] ),
|
|
sanitize_hex_color( $s['button_color'] ),
|
|
sanitize_hex_color( $s['button_border'] ),
|
|
absint( $s['button_radius'] )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Output above the form (via login_message).
|
|
*
|
|
* @param string $message Existing message HTML.
|
|
* @return string
|
|
*/
|
|
public function render_above( $message ) {
|
|
if ( 'above' !== $this->settings->get( 'button_position' ) || ! $this->should_render() ) {
|
|
return $message;
|
|
}
|
|
return $message . $this->markup( 'above' );
|
|
}
|
|
|
|
/**
|
|
* Output below the form (moved into place by login.js).
|
|
*/
|
|
public function render_below() {
|
|
if ( 'below' !== $this->settings->get( 'button_position' ) || ! $this->should_render() ) {
|
|
return;
|
|
}
|
|
echo $this->markup( 'below' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- markup() escapes everything.
|
|
}
|
|
|
|
/**
|
|
* Shortcode for placing the button on custom login pages.
|
|
*
|
|
* @param array $atts Attributes.
|
|
* @return string
|
|
*/
|
|
public function shortcode( $atts ) {
|
|
$atts = shortcode_atts(
|
|
array(
|
|
'redirect' => '',
|
|
'messages' => 'yes',
|
|
'divider' => 'no',
|
|
),
|
|
$atts,
|
|
'm365_login_button'
|
|
);
|
|
return $this->standalone(
|
|
array(
|
|
'redirect' => $atts['redirect'],
|
|
'show_messages' => 'no' !== strtolower( (string) $atts['messages'] ),
|
|
'show_divider' => 'yes' === strtolower( (string) $atts['divider'] ),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Full block: divider + button.
|
|
*
|
|
* @param string $position 'above' or 'below'.
|
|
* @return string
|
|
*/
|
|
public function markup( $position ) {
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- passed through to the flow, validated there.
|
|
$redirect_to = isset( $_REQUEST['redirect_to'] ) ? esc_url_raw( wp_unslash( $_REQUEST['redirect_to'] ) ) : '';
|
|
|
|
$divider = $this->form_hidden() ? '' : $this->divider_markup();
|
|
|
|
$html = '<div class="m365-login m365-login--' . esc_attr( $position ) . '" id="m365-login-block">';
|
|
$html .= 'above' === $position ? $this->button( $redirect_to ) . $divider : $divider . $this->button( $redirect_to );
|
|
$html .= '</div>';
|
|
return $html;
|
|
}
|
|
|
|
/**
|
|
* Button markup.
|
|
*
|
|
* @param string $redirect_to Post-login destination.
|
|
* @return string
|
|
*/
|
|
public function button( $redirect_to = '' ) {
|
|
$auth = M365_Login::instance()->auth;
|
|
$url = $auth->start_url( $redirect_to );
|
|
|
|
$icon = '';
|
|
if ( $this->settings->get( 'button_show_icon' ) ) {
|
|
$custom = (string) $this->settings->get( 'button_icon' );
|
|
if ( '' !== $custom && M365_Login_Settings::is_safe_image_url( $custom ) ) {
|
|
$icon = '<img class="m365-login__icon" src="' . esc_url( $custom ) . '" alt="" width="20" height="20" loading="lazy" />';
|
|
} else {
|
|
$icon = self::microsoft_logo();
|
|
}
|
|
}
|
|
|
|
return '<a class="m365-login__button" href="' . esc_url( $url ) . '" rel="nofollow">'
|
|
. $icon
|
|
. '<span class="m365-login__label">' . esc_html( $this->settings->get( 'button_text' ) ) . '</span>'
|
|
. '</a>';
|
|
}
|
|
|
|
/**
|
|
* Bundled Microsoft logo (inline SVG, four coloured squares).
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function microsoft_logo() {
|
|
return '<svg class="m365-login__icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23" width="20" height="20" aria-hidden="true" focusable="false">'
|
|
. '<path fill="#f25022" d="M1 1h10v10H1z"/>'
|
|
. '<path fill="#7fba00" d="M12 1h10v10H12z"/>'
|
|
. '<path fill="#00a4ef" d="M1 12h10v10H1z"/>'
|
|
. '<path fill="#ffb900" d="M12 12h10v10H12z"/>'
|
|
. '</svg>';
|
|
}
|
|
}
|