- Add the button (and error messages) to every wp_login_form() form via login_form_top/login_form_bottom; in button-only mode the password fields are wrapped and hidden there. - New template functions m365_login_button() and m365_login_messages(); the shortcode gains divider and messages attributes. - New setting for the custom login page URL: failed sign-ins, the fallback link and the logout redirect point there instead of wp-login.php. Must be a same-site URL. - Button-only mode now blocks every interactive password sign-in through the authenticate filter, not only wp-login.php; XML-RPC, REST, WP-CLI and cron are exempt, plus a filter for trusted exceptions. - Fallback key accepted on any page (init) instead of login_init only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJxAHYdMfKPoN4koRc4Ci2
607 lines
36 KiB
PHP
607 lines
36 KiB
PHP
<?php
|
||
/**
|
||
* Admin settings screen.
|
||
*
|
||
* @package M365_Login
|
||
*/
|
||
|
||
defined( 'ABSPATH' ) || exit;
|
||
|
||
/**
|
||
* Registers and renders the settings page.
|
||
*/
|
||
class M365_Login_Admin {
|
||
|
||
const PAGE = 'm365-login';
|
||
const GROUP = 'm365_login';
|
||
const AJAX_TEST = 'm365_login_test_connection';
|
||
const AJAX_GROUPS = 'm365_login_search_groups';
|
||
const NONCE_TEST = 'm365_login_test';
|
||
|
||
/**
|
||
* Settings.
|
||
*
|
||
* @var M365_Login_Settings
|
||
*/
|
||
private $settings;
|
||
|
||
/**
|
||
* Auth component (for endpoint URLs and discovery).
|
||
*
|
||
* @var M365_Login_Auth
|
||
*/
|
||
private $auth;
|
||
|
||
/**
|
||
* Graph client.
|
||
*
|
||
* @var M365_Login_Graph
|
||
*/
|
||
private $graph;
|
||
|
||
/**
|
||
* Screen hook suffix.
|
||
*
|
||
* @var string
|
||
*/
|
||
private $hook = '';
|
||
|
||
/**
|
||
* Constructor.
|
||
*
|
||
* @param M365_Login_Settings $settings Settings.
|
||
* @param M365_Login_Auth $auth Auth.
|
||
* @param M365_Login_Graph $graph Graph client.
|
||
*/
|
||
public function __construct( M365_Login_Settings $settings, M365_Login_Auth $auth, M365_Login_Graph $graph ) {
|
||
$this->settings = $settings;
|
||
$this->auth = $auth;
|
||
$this->graph = $graph;
|
||
|
||
add_action( 'admin_menu', array( $this, 'menu' ) );
|
||
add_action( 'admin_init', array( $this, 'register' ) );
|
||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) );
|
||
add_action( 'wp_ajax_' . self::AJAX_TEST, array( $this, 'ajax_test_connection' ) );
|
||
add_action( 'wp_ajax_' . self::AJAX_GROUPS, array( $this, 'ajax_search_groups' ) );
|
||
add_action( 'update_option_' . M365_LOGIN_OPTION, array( $this->graph, 'flush_token' ) );
|
||
add_action( 'admin_notices', array( $this, 'setup_notice' ) );
|
||
}
|
||
|
||
/**
|
||
* Adds the menu entry under Settings.
|
||
*/
|
||
public function menu() {
|
||
$this->hook = add_options_page(
|
||
__( 'M365 Login', 'm365-login' ),
|
||
__( 'M365 Login', 'm365-login' ),
|
||
'manage_options',
|
||
self::PAGE,
|
||
array( $this, 'render' )
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Registers the option with the Settings API.
|
||
*/
|
||
public function register() {
|
||
register_setting(
|
||
self::GROUP,
|
||
M365_LOGIN_OPTION,
|
||
array(
|
||
'type' => 'array',
|
||
'sanitize_callback' => array( $this->settings, 'sanitize' ),
|
||
'default' => $this->settings->defaults(),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Nudges administrators to finish the setup.
|
||
*/
|
||
public function setup_notice() {
|
||
if ( $this->settings->is_configured() || ! current_user_can( 'manage_options' ) ) {
|
||
return;
|
||
}
|
||
$screen = get_current_screen();
|
||
if ( $screen && $this->hook === $screen->id ) {
|
||
return;
|
||
}
|
||
if ( ! $screen || ! in_array( $screen->id, array( 'plugins', 'dashboard' ), true ) ) {
|
||
return;
|
||
}
|
||
printf(
|
||
'<div class="notice notice-info is-dismissible"><p>%s <a href="%s">%s</a></p></div>',
|
||
esc_html__( 'M365 Login is active but not connected to Microsoft Entra ID yet.', 'm365-login' ),
|
||
esc_url( admin_url( 'options-general.php?page=' . self::PAGE ) ),
|
||
esc_html__( 'Open the settings', 'm365-login' )
|
||
);
|
||
}
|
||
|
||
/**
|
||
* Loads assets on our screen only.
|
||
*
|
||
* @param string $hook Current screen hook.
|
||
*/
|
||
public function enqueue( $hook ) {
|
||
if ( $hook !== $this->hook ) {
|
||
return;
|
||
}
|
||
|
||
wp_enqueue_media();
|
||
wp_enqueue_style( 'wp-color-picker' );
|
||
wp_enqueue_style( 'm365-login-admin', M365_LOGIN_URL . 'assets/css/admin.css', array( 'wp-color-picker' ), M365_LOGIN_VERSION );
|
||
wp_enqueue_script( 'm365-login-admin', M365_LOGIN_URL . 'assets/js/admin.js', array( 'jquery', 'wp-color-picker' ), M365_LOGIN_VERSION, true );
|
||
|
||
wp_localize_script(
|
||
'm365-login-admin',
|
||
'm365LoginAdmin',
|
||
array(
|
||
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
|
||
'nonce' => wp_create_nonce( self::NONCE_TEST ),
|
||
'action' => self::AJAX_TEST,
|
||
'groupAction' => self::AJAX_GROUPS,
|
||
'defaultLogo' => M365_Login_Button::microsoft_logo(),
|
||
'i18n' => array(
|
||
'chooseIcon' => __( 'Choose button icon', 'm365-login' ),
|
||
'useIcon' => __( 'Use this icon', 'm365-login' ),
|
||
'copied' => __( 'Copied!', 'm365-login' ),
|
||
'copy' => __( 'Copy', 'm365-login' ),
|
||
'testing' => __( 'Testing…', 'm365-login' ),
|
||
'testFailed' => __( 'The tenant could not be reached. Check the tenant ID and the server’s outgoing connections.', 'm365-login' ),
|
||
'noGroups' => __( 'No groups found.', 'm365-login' ),
|
||
'searching' => __( 'Searching…', 'm365-login' ),
|
||
'add' => __( 'Add', 'm365-login' ),
|
||
'remove' => __( 'Remove', 'm365-login' ),
|
||
'saveFirst' => __( 'Save the connection settings first, then search for groups.', 'm365-login' ),
|
||
'confirmKey' => __( 'Generate a new fallback key on save? The old link stops working.', 'm365-login' ),
|
||
),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* AJAX: fetch the OpenID configuration for the tenant typed into the form.
|
||
*/
|
||
public function ajax_test_connection() {
|
||
check_ajax_referer( self::NONCE_TEST, 'nonce' );
|
||
if ( ! current_user_can( 'manage_options' ) ) {
|
||
wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'm365-login' ) ), 403 );
|
||
}
|
||
|
||
$tenant = isset( $_POST['tenant'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['tenant'] ) ) ) : '';
|
||
if ( '' === $tenant || ! M365_Login_Settings::is_valid_tenant( $tenant ) ) {
|
||
wp_send_json_error( array( 'message' => __( 'Please enter a valid tenant ID first.', 'm365-login' ) ) );
|
||
}
|
||
|
||
$url = 'https://login.microsoftonline.com/' . rawurlencode( $tenant ) . '/v2.0/.well-known/openid-configuration';
|
||
$response = wp_remote_get( $url, array( 'timeout' => M365_Login_Auth::HTTP_TIMEOUT ) );
|
||
if ( is_wp_error( $response ) ) {
|
||
wp_send_json_error( array( 'message' => $response->get_error_message() ) );
|
||
}
|
||
$code = (int) wp_remote_retrieve_response_code( $response );
|
||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||
if ( 200 !== $code || ! is_array( $body ) || empty( $body['issuer'] ) ) {
|
||
wp_send_json_error(
|
||
array(
|
||
/* translators: %d: HTTP status code */
|
||
'message' => sprintf( __( 'Microsoft answered with HTTP %d. Is the tenant ID correct?', 'm365-login' ), $code ),
|
||
)
|
||
);
|
||
}
|
||
|
||
wp_send_json_success(
|
||
array(
|
||
'issuer' => esc_url_raw( $body['issuer'] ),
|
||
'endpoint' => isset( $body['authorization_endpoint'] ) ? esc_url_raw( $body['authorization_endpoint'] ) : '',
|
||
'message' => __( 'Tenant reachable. The OpenID configuration was loaded successfully.', 'm365-login' ),
|
||
)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* AJAX: search Entra groups through Microsoft Graph.
|
||
*/
|
||
public function ajax_search_groups() {
|
||
check_ajax_referer( self::NONCE_TEST, 'nonce' );
|
||
if ( ! current_user_can( 'manage_options' ) ) {
|
||
wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'm365-login' ) ), 403 );
|
||
}
|
||
if ( ! $this->settings->is_configured() ) {
|
||
wp_send_json_error( array( 'message' => __( 'Save the connection settings first, then search for groups.', 'm365-login' ) ) );
|
||
}
|
||
|
||
$query = isset( $_POST['query'] ) ? sanitize_text_field( wp_unslash( $_POST['query'] ) ) : '';
|
||
$groups = $this->graph->search_groups( mb_substr( $query, 0, 100 ) );
|
||
|
||
if ( is_wp_error( $groups ) ) {
|
||
$message = $groups->get_error_message();
|
||
if ( false !== stripos( $message, 'Authorization_RequestDenied' ) || false !== stripos( $message, 'Insufficient privileges' ) ) {
|
||
$message = __( 'Microsoft Graph refused the request. Grant the application permission "GroupMember.Read.All" (or "Directory.Read.All") with admin consent in Entra ID.', 'm365-login' );
|
||
}
|
||
wp_send_json_error( array( 'message' => $message ) );
|
||
}
|
||
|
||
wp_send_json_success( array( 'groups' => $groups ) );
|
||
}
|
||
|
||
/**
|
||
* Renders the settings screen.
|
||
*/
|
||
public function render() {
|
||
if ( ! current_user_can( 'manage_options' ) ) {
|
||
wp_die( esc_html__( 'You are not allowed to access this page.', 'm365-login' ) );
|
||
}
|
||
|
||
$s = $this->settings->all();
|
||
$configured = $this->settings->is_configured();
|
||
$has_secret = '' !== $this->settings->client_secret();
|
||
$option = M365_LOGIN_OPTION;
|
||
$field = function ( $key ) use ( $option ) {
|
||
return esc_attr( $option . '[' . $key . ']' );
|
||
};
|
||
?>
|
||
<div class="wrap m365-admin">
|
||
<header class="m365-admin__header">
|
||
<div class="m365-admin__brand">
|
||
<span class="m365-admin__logo"><?php echo M365_Login_Button::microsoft_logo(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG. ?></span>
|
||
<div>
|
||
<h1><?php esc_html_e( 'M365 Login', 'm365-login' ); ?></h1>
|
||
<p><?php esc_html_e( 'Let existing users sign in with their Microsoft 365 / Entra ID account.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
<span class="m365-admin__status <?php echo $configured ? 'is-ok' : 'is-pending'; ?>">
|
||
<span class="m365-admin__status-dot"></span>
|
||
<?php echo $configured ? esc_html__( 'Connected', 'm365-login' ) : esc_html__( 'Setup incomplete', 'm365-login' ); ?>
|
||
</span>
|
||
</header>
|
||
|
||
<form method="post" action="options.php" class="m365-admin__form" novalidate>
|
||
<?php settings_fields( self::GROUP ); ?>
|
||
|
||
<nav class="m365-admin__tabs" role="tablist">
|
||
<button type="button" class="m365-admin__tab is-active" role="tab" data-tab="connection" aria-selected="true"><?php esc_html_e( 'Connection', 'm365-login' ); ?></button>
|
||
<button type="button" class="m365-admin__tab" role="tab" data-tab="button" aria-selected="false"><?php esc_html_e( 'Button', 'm365-login' ); ?></button>
|
||
<button type="button" class="m365-admin__tab" role="tab" data-tab="security" aria-selected="false"><?php esc_html_e( 'Security', 'm365-login' ); ?></button>
|
||
</nav>
|
||
|
||
<div class="m365-admin__layout">
|
||
<div class="m365-admin__main">
|
||
|
||
<!-- Connection -->
|
||
<section class="m365-admin__panel is-active" data-panel="connection">
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Microsoft Entra ID app registration', 'm365-login' ); ?></h2>
|
||
<p class="m365-card__intro"><?php esc_html_e( 'Enter the values from your app registration in the Microsoft Entra admin center.', 'm365-login' ); ?></p>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-tenant"><?php esc_html_e( 'Directory (tenant) ID', 'm365-login' ); ?></label>
|
||
<div class="m365-field__row">
|
||
<input type="text" id="m365-tenant" name="<?php echo $field( 'tenant_id' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s['tenant_id'] ); ?>" class="regular-text code" placeholder="00000000-0000-0000-0000-000000000000" autocomplete="off" spellcheck="false" />
|
||
<button type="button" class="button" id="m365-test"><?php esc_html_e( 'Test tenant', 'm365-login' ); ?></button>
|
||
</div>
|
||
<p class="description"><?php esc_html_e( 'Recommended: the GUID of your tenant. Only sign-ins from this tenant are accepted. "organizations" allows any work or school account.', 'm365-login' ); ?></p>
|
||
<div id="m365-test-result" class="m365-inline-result" hidden></div>
|
||
</div>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-client-id"><?php esc_html_e( 'Application (client) ID', 'm365-login' ); ?></label>
|
||
<input type="text" id="m365-client-id" name="<?php echo $field( 'client_id' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s['client_id'] ); ?>" class="regular-text code" placeholder="00000000-0000-0000-0000-000000000000" autocomplete="off" spellcheck="false" />
|
||
</div>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-client-secret"><?php esc_html_e( 'Client secret', 'm365-login' ); ?></label>
|
||
<div class="m365-field__row">
|
||
<input type="password" id="m365-client-secret" name="<?php echo $field( 'client_secret' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="" class="regular-text code" autocomplete="new-password" placeholder="<?php echo $has_secret ? esc_attr__( '•••••••••••• (stored, leave empty to keep)', 'm365-login' ) : esc_attr__( 'Paste the secret value', 'm365-login' ); ?>" />
|
||
<button type="button" class="button m365-toggle-secret" aria-label="<?php esc_attr_e( 'Show secret', 'm365-login' ); ?>"><span class="dashicons dashicons-visibility"></span></button>
|
||
</div>
|
||
<?php if ( $has_secret ) : ?>
|
||
<label class="m365-check m365-check--inline">
|
||
<input type="checkbox" name="<?php echo $field( 'client_secret_clear' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" />
|
||
<?php esc_html_e( 'Remove the stored secret', 'm365-login' ); ?>
|
||
</label>
|
||
<?php endif; ?>
|
||
<p class="description"><?php esc_html_e( 'Stored encrypted (AES-256-GCM, key derived from your WordPress salts) and never displayed again. Client secrets expire – note the expiry date in Entra ID.', 'm365-login' ); ?></p>
|
||
</div>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-prompt"><?php esc_html_e( 'Account prompt', 'm365-login' ); ?></label>
|
||
<select id="m365-prompt" name="<?php echo $field( 'prompt' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>">
|
||
<option value="select_account" <?php selected( $s['prompt'], 'select_account' ); ?>><?php esc_html_e( 'Always let the user pick an account (recommended)', 'm365-login' ); ?></option>
|
||
<option value="none" <?php selected( $s['prompt'], 'none' ); ?>><?php esc_html_e( 'Use the current Microsoft session if available', 'm365-login' ); ?></option>
|
||
<option value="login" <?php selected( $s['prompt'], 'login' ); ?>><?php esc_html_e( 'Always require re-entering credentials', 'm365-login' ); ?></option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Button -->
|
||
<section class="m365-admin__panel" data-panel="button">
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Appearance', 'm365-login' ); ?></h2>
|
||
|
||
<div class="m365-preview">
|
||
<span class="m365-preview__label"><?php esc_html_e( 'Live preview', 'm365-login' ); ?></span>
|
||
<div class="m365-preview__stage">
|
||
<div class="m365-login m365-login--preview" id="m365-preview" style="<?php echo esc_attr( str_replace( array( '.m365-login{', '}' ), '', M365_Login::instance()->button->css_variables() ) ); ?>">
|
||
<div class="m365-login__divider"><span id="m365-preview-divider"><?php echo esc_html( $s['divider_text'] ); ?></span></div>
|
||
<a class="m365-login__button" href="#" onclick="return false;" id="m365-preview-button">
|
||
<span id="m365-preview-icon"><?php echo M365_Login_Button::microsoft_logo(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG. ?></span>
|
||
<span class="m365-login__label" id="m365-preview-text"><?php echo esc_html( $s['button_text'] ); ?></span>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-grid">
|
||
<div class="m365-field">
|
||
<label for="m365-button-text"><?php esc_html_e( 'Button text', 'm365-login' ); ?></label>
|
||
<input type="text" id="m365-button-text" name="<?php echo $field( 'button_text' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s['button_text'] ); ?>" class="regular-text" maxlength="80" data-preview="text" />
|
||
</div>
|
||
<div class="m365-field">
|
||
<label for="m365-divider-text"><?php esc_html_e( 'Divider text', 'm365-login' ); ?></label>
|
||
<input type="text" id="m365-divider-text" name="<?php echo $field( 'divider_text' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s['divider_text'] ); ?>" class="regular-text" maxlength="40" data-preview="divider" />
|
||
<p class="description"><?php esc_html_e( 'Leave empty to hide the divider line.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-field">
|
||
<span class="m365-field__label"><?php esc_html_e( 'Icon', 'm365-login' ); ?></span>
|
||
<label class="m365-check">
|
||
<input type="checkbox" name="<?php echo $field( 'button_show_icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['button_show_icon'] ); ?> data-preview="show-icon" />
|
||
<?php esc_html_e( 'Show an icon on the button', 'm365-login' ); ?>
|
||
</label>
|
||
<div class="m365-icon-picker">
|
||
<div class="m365-icon-picker__thumb" id="m365-icon-thumb">
|
||
<?php if ( '' !== $s['button_icon'] ) : ?>
|
||
<img src="<?php echo esc_url( $s['button_icon'] ); ?>" alt="" />
|
||
<?php else : ?>
|
||
<?php echo M365_Login_Button::microsoft_logo(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG. ?>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="m365-icon-picker__controls">
|
||
<input type="url" id="m365-icon-url" name="<?php echo $field( 'button_icon' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_url( $s['button_icon'] ); ?>" class="regular-text code" placeholder="<?php esc_attr_e( 'Default: Microsoft logo', 'm365-login' ); ?>" data-preview="icon" />
|
||
<div class="m365-field__row">
|
||
<button type="button" class="button" id="m365-icon-choose"><?php esc_html_e( 'Choose from media library', 'm365-login' ); ?></button>
|
||
<button type="button" class="button-link m365-link-danger" id="m365-icon-reset"><?php esc_html_e( 'Use Microsoft logo', 'm365-login' ); ?></button>
|
||
</div>
|
||
<p class="description"><?php esc_html_e( 'PNG, SVG, JPG or WebP. Square images (e.g. 64×64 px) work best.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-grid m365-grid--4">
|
||
<?php
|
||
$colors = array(
|
||
'button_bg' => __( 'Background', 'm365-login' ),
|
||
'button_bg_hover' => __( 'Background (hover)', 'm365-login' ),
|
||
'button_color' => __( 'Text colour', 'm365-login' ),
|
||
'button_border' => __( 'Border', 'm365-login' ),
|
||
);
|
||
foreach ( $colors as $key => $label ) :
|
||
?>
|
||
<div class="m365-field">
|
||
<label for="m365-<?php echo esc_attr( $key ); ?>"><?php echo esc_html( $label ); ?></label>
|
||
<input type="text" id="m365-<?php echo esc_attr( $key ); ?>" name="<?php echo $field( $key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s[ $key ] ); ?>" class="m365-color" data-default-color="<?php echo esc_attr( $this->settings->defaults()[ $key ] ); ?>" data-preview="<?php echo esc_attr( str_replace( 'button_', '', $key ) ); ?>" />
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
|
||
<div class="m365-grid">
|
||
<div class="m365-field">
|
||
<label for="m365-radius"><?php esc_html_e( 'Corner radius', 'm365-login' ); ?> <span class="m365-range-value" id="m365-radius-value"><?php echo esc_html( $s['button_radius'] ); ?> px</span></label>
|
||
<input type="range" id="m365-radius" name="<?php echo $field( 'button_radius' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_attr( $s['button_radius'] ); ?>" min="0" max="50" step="1" data-preview="radius" />
|
||
</div>
|
||
<div class="m365-field">
|
||
<label for="m365-position"><?php esc_html_e( 'Position on the login page', 'm365-login' ); ?></label>
|
||
<select id="m365-position" name="<?php echo $field( 'button_position' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>">
|
||
<option value="below" <?php selected( $s['button_position'], 'below' ); ?>><?php esc_html_e( 'Below the login form', 'm365-login' ); ?></option>
|
||
<option value="above" <?php selected( $s['button_position'], 'above' ); ?>><?php esc_html_e( 'Above the login form', 'm365-login' ); ?></option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-presets">
|
||
<span class="m365-field__label"><?php esc_html_e( 'Quick presets', 'm365-login' ); ?></span>
|
||
<button type="button" class="m365-preset" data-preset='{"bg":"#2f2f2f","bg_hover":"#1a1a1a","color":"#ffffff","border":"#2f2f2f"}'><span style="background:#2f2f2f"></span><?php esc_html_e( 'Microsoft dark', 'm365-login' ); ?></button>
|
||
<button type="button" class="m365-preset" data-preset='{"bg":"#ffffff","bg_hover":"#f3f3f3","color":"#5e5e5e","border":"#8c8c8c"}'><span style="background:#ffffff;border-color:#8c8c8c"></span><?php esc_html_e( 'Microsoft light', 'm365-login' ); ?></button>
|
||
<button type="button" class="m365-preset" data-preset='{"bg":"#0078d4","bg_hover":"#106ebe","color":"#ffffff","border":"#0078d4"}'><span style="background:#0078d4"></span><?php esc_html_e( 'Azure blue', 'm365-login' ); ?></button>
|
||
<button type="button" class="m365-preset" data-preset='{"bg":"#2271b1","bg_hover":"#135e96","color":"#ffffff","border":"#2271b1"}'><span style="background:#2271b1"></span><?php esc_html_e( 'WordPress blue', 'm365-login' ); ?></button>
|
||
</div>
|
||
</div>
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Custom login page', 'm365-login' ); ?></h2>
|
||
<p class="m365-card__intro"><?php esc_html_e( 'Using your own login page instead of wp-login.php? Tell the plugin where it is so error messages, the fallback link and the post-logout redirect point there.', 'm365-login' ); ?></p>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-custom-login"><?php esc_html_e( 'URL of your login page', 'm365-login' ); ?></label>
|
||
<input type="url" id="m365-custom-login" name="<?php echo $field( 'custom_login_url' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="<?php echo esc_url( $s['custom_login_url'] ); ?>" class="regular-text code" placeholder="<?php echo esc_attr( home_url( '/login/' ) ); ?>" />
|
||
<p class="description"><?php esc_html_e( 'Must be on this site. Leave empty to use wp-login.php.', 'm365-login' ); ?></p>
|
||
</div>
|
||
|
||
<label class="m365-check m365-check--block">
|
||
<input type="checkbox" name="<?php echo $field( 'inject_form' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['inject_form'] ); ?> />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Add the button to every wp_login_form() form automatically', 'm365-login' ); ?></strong>
|
||
<em><?php esc_html_e( 'Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
|
||
<div class="m365-field">
|
||
<span class="m365-field__label"><?php esc_html_e( 'Manual placement', 'm365-login' ); ?></span>
|
||
<p class="description"><?php esc_html_e( 'Shortcode (block editor, page builders):', 'm365-login' ); ?></p>
|
||
<code>[m365_login_button redirect="/dashboard/" divider="yes"]</code>
|
||
<p class="description"><?php esc_html_e( 'Template function (theme files):', 'm365-login' ); ?></p>
|
||
<code><?php m365_login_button( array( 'redirect' => '/dashboard/' ) ); ?></code>
|
||
<p class="description"><?php esc_html_e( 'Both show the error messages of the last attempt; use m365_login_messages() to place them separately.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Security -->
|
||
<section class="m365-admin__panel" data-panel="security">
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'User matching & hardening', 'm365-login' ); ?></h2>
|
||
<p class="m365-card__intro"><?php esc_html_e( 'Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists.', 'm365-login' ); ?></p>
|
||
|
||
<label class="m365-check m365-check--block">
|
||
<input type="checkbox" name="<?php echo $field( 'bind_oid' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['bind_oid'] ); ?> />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Bind WordPress accounts to the Microsoft object ID', 'm365-login' ); ?></strong>
|
||
<em><?php esc_html_e( 'On first sign-in the immutable Microsoft object ID is stored with the user. Later sign-ins with the same e-mail but a different Microsoft identity are rejected. Strongly recommended.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
|
||
<label class="m365-check m365-check--block">
|
||
<input type="checkbox" name="<?php echo $field( 'upn_fallback' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['upn_fallback'] ); ?> />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Fall back to the user principal name (UPN)', 'm365-login' ); ?></strong>
|
||
<em><?php esc_html_e( 'If the token contains no "email" claim, use the UPN (e.g. jane@contoso.com) when it is a valid e-mail address. Usually required for work accounts.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
|
||
<label class="m365-check m365-check--block">
|
||
<input type="checkbox" name="<?php echo $field( 'remember_me' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['remember_me'] ); ?> />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Keep users signed in ("Remember me")', 'm365-login' ); ?></strong>
|
||
<em><?php esc_html_e( 'Issues a 14-day WordPress session instead of a browser session.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-domains"><?php esc_html_e( 'Allowed e-mail domains (optional)', 'm365-login' ); ?></label>
|
||
<textarea id="m365-domains" name="<?php echo $field( 'allowed_domains' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" rows="3" class="large-text code" placeholder="contoso.com, contoso.de"><?php echo esc_textarea( $s['allowed_domains'] ); ?></textarea>
|
||
<p class="description"><?php esc_html_e( 'One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Allowed Entra groups (optional)', 'm365-login' ); ?></h2>
|
||
<p class="m365-card__intro"><?php esc_html_e( 'Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count.', 'm365-login' ); ?></p>
|
||
|
||
<div class="m365-field">
|
||
<label for="m365-group-search"><?php esc_html_e( 'Search groups', 'm365-login' ); ?></label>
|
||
<div class="m365-field__row">
|
||
<input type="search" id="m365-group-search" class="regular-text" placeholder="<?php esc_attr_e( 'Type a group name or paste an object ID…', 'm365-login' ); ?>" autocomplete="off" <?php disabled( ! $configured ); ?> />
|
||
<button type="button" class="button" id="m365-group-search-btn" <?php disabled( ! $configured ); ?>><?php esc_html_e( 'Search', 'm365-login' ); ?></button>
|
||
</div>
|
||
<?php if ( ! $configured ) : ?>
|
||
<p class="description"><?php esc_html_e( 'Save the connection settings first, then search for groups.', 'm365-login' ); ?></p>
|
||
<?php else : ?>
|
||
<p class="description"><?php esc_html_e( 'Needs the application permission "GroupMember.Read.All" with admin consent. Without it you can still paste group object IDs.', 'm365-login' ); ?></p>
|
||
<?php endif; ?>
|
||
<div id="m365-group-results" class="m365-group-results" hidden></div>
|
||
</div>
|
||
|
||
<div class="m365-field">
|
||
<span class="m365-field__label"><?php esc_html_e( 'Selected groups', 'm365-login' ); ?></span>
|
||
<ul id="m365-group-list" class="m365-group-list" data-empty="<?php esc_attr_e( 'No groups selected – every matched user may sign in.', 'm365-login' ); ?>">
|
||
<?php foreach ( $this->settings->allowed_groups() as $gid => $gname ) : ?>
|
||
<li class="m365-group-chip" data-id="<?php echo esc_attr( $gid ); ?>">
|
||
<span class="m365-group-chip__name"><?php echo esc_html( $gname ); ?></span>
|
||
<code class="m365-group-chip__id"><?php echo esc_html( $gid ); ?></code>
|
||
<input type="hidden" name="<?php echo esc_attr( $option . '[allowed_groups][' . $gid . ']' ); ?>" value="<?php echo esc_attr( $gname ); ?>" />
|
||
<button type="button" class="m365-group-chip__remove" aria-label="<?php esc_attr_e( 'Remove', 'm365-login' ); ?>">×</button>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<p class="description"><?php esc_html_e( 'Membership is read from the "groups" claim of the ID token when present; otherwise the plugin asks Microsoft Graph (application permission "User.Read.All" or "Directory.Read.All"). If neither works, the sign-in is refused.', 'm365-login' ); ?></p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Button-only mode', 'm365-login' ); ?></h2>
|
||
<p class="m365-card__intro"><?php esc_html_e( 'Hides the username/password fields (on wp-login.php and in wp_login_form() forms) and refuses every interactive password sign-in on the site, including custom login forms. Application passwords, REST, XML-RPC and WP-CLI are not affected.', 'm365-login' ); ?></p>
|
||
|
||
<label class="m365-check m365-check--block">
|
||
<input type="checkbox" name="<?php echo $field( 'button_only' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" <?php checked( $s['button_only'] ); ?> id="m365-button-only" />
|
||
<span>
|
||
<strong><?php esc_html_e( 'Show only the Microsoft button on the login page', 'm365-login' ); ?></strong>
|
||
<em><?php esc_html_e( 'Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this.', 'm365-login' ); ?></em>
|
||
</span>
|
||
</label>
|
||
|
||
<div class="m365-fallback">
|
||
<span class="m365-field__label"><?php esc_html_e( 'Fallback link (keep it secret)', 'm365-login' ); ?></span>
|
||
<p class="description"><?php esc_html_e( 'Opening this link shows the password form again in that browser for 30 minutes and allows password sign-in there. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks.', 'm365-login' ); ?></p>
|
||
<?php if ( '' !== $this->settings->fallback_url() ) : ?>
|
||
<div class="m365-copy">
|
||
<code id="m365-fallback-url"><?php echo esc_html( $this->settings->fallback_url() ); ?></code>
|
||
<button type="button" class="button button-small m365-copy__button" data-copy="m365-fallback-url"><?php esc_html_e( 'Copy', 'm365-login' ); ?></button>
|
||
</div>
|
||
<label class="m365-check m365-check--inline">
|
||
<input type="checkbox" name="<?php echo $field( 'fallback_regenerate' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>" value="1" id="m365-fallback-regenerate" />
|
||
<?php esc_html_e( 'Generate a new key when saving', 'm365-login' ); ?>
|
||
</label>
|
||
<?php else : ?>
|
||
<p class="m365-inline-result"><?php esc_html_e( 'A key is generated automatically the first time you save these settings.', 'm365-login' ); ?></p>
|
||
<?php endif; ?>
|
||
<p class="description">
|
||
<?php
|
||
printf(
|
||
/* translators: %s: PHP constant */
|
||
esc_html__( 'Emergency switch: add %s to wp-config.php to disable button-only mode entirely.', 'm365-login' ),
|
||
'<code>define( \'M365_LOGIN_DISABLE_BUTTON_ONLY\', true );</code>'
|
||
);
|
||
?>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="m365-card m365-card--muted">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'What the plugin does to keep sign-ins safe', 'm365-login' ); ?></h2>
|
||
<ul class="m365-list">
|
||
<li><?php esc_html_e( 'OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection).', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Client secret encrypted at rest; no accounts are created, no passwords are changed.', 'm365-login' ); ?></li>
|
||
</ul>
|
||
</div>
|
||
</section>
|
||
|
||
<div class="m365-admin__actions">
|
||
<?php submit_button( __( 'Save changes', 'm365-login' ), 'primary large', 'submit', false ); ?>
|
||
</div>
|
||
</div>
|
||
|
||
<aside class="m365-admin__sidebar">
|
||
<div class="m365-card m365-card--accent">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Redirect URI', 'm365-login' ); ?></h2>
|
||
<p><?php esc_html_e( 'Register this URI in your app registration under Authentication → Web → Redirect URIs:', 'm365-login' ); ?></p>
|
||
<div class="m365-copy">
|
||
<code id="m365-redirect-uri"><?php echo esc_html( $this->settings->redirect_uri() ); ?></code>
|
||
<button type="button" class="button button-small m365-copy__button" data-copy="m365-redirect-uri"><?php esc_html_e( 'Copy', 'm365-login' ); ?></button>
|
||
</div>
|
||
<?php if ( ! $this->settings->uses_pretty_callback() ) : ?>
|
||
<p class="description"><?php esc_html_e( 'Plain permalinks are active, so the callback uses a query string. If you enable pretty permalinks later, the redirect URI changes and must be updated in Entra ID.', 'm365-login' ); ?></p>
|
||
<?php endif; ?>
|
||
<?php if ( ! is_ssl() && 'https' !== wp_parse_url( home_url(), PHP_URL_SCHEME ) ) : ?>
|
||
<p class="m365-warning"><?php esc_html_e( 'Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS.', 'm365-login' ); ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="m365-card">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Setup in 5 steps', 'm365-login' ); ?></h2>
|
||
<ol class="m365-steps">
|
||
<li><?php esc_html_e( 'Open the Microsoft Entra admin center → App registrations → New registration.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Choose "Accounts in this organizational directory only", set the platform to Web and paste the redirect URI above.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Copy the Application (client) ID and Directory (tenant) ID from the overview page.', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Under Certificates & secrets create a client secret and copy its value (not the ID).', 'm365-login' ); ?></li>
|
||
<li><?php esc_html_e( 'Under Token configuration add the optional claim "email" for ID tokens (recommended), then save this page.', 'm365-login' ); ?></li>
|
||
</ol>
|
||
<p class="description"><?php esc_html_e( 'Required API permission: openid, profile, email (delegated) – granted by default.', 'm365-login' ); ?></p>
|
||
<p class="description"><?php esc_html_e( 'Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent.', 'm365-login' ); ?></p>
|
||
</div>
|
||
|
||
<div class="m365-card m365-card--muted">
|
||
<h2 class="m365-card__title"><?php esc_html_e( 'Shortcode', 'm365-login' ); ?></h2>
|
||
<p><?php esc_html_e( 'Place the button on a custom login page:', 'm365-login' ); ?></p>
|
||
<code>[m365_login_button redirect="/my-account/"]</code>
|
||
<p class="description"><?php esc_html_e( 'More options on the Button tab under "Custom login page".', 'm365-login' ); ?></p>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<?php
|
||
}
|
||
}
|