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( 'wp_ajax_' . self::AJAX_CERT, array( $this, 'ajax_certificate' ) );
add_action( 'admin_post_' . self::POST_CERT, array( $this, 'download_certificate' ) );
add_action( 'update_option_' . M365_LOGIN_OPTION, array( $this->graph, 'flush_token' ) );
add_action( 'admin_notices', array( $this, 'setup_notice' ) );
add_filter( 'submenu_file', array( $this, 'highlight_submenu' ) );
}
/**
* Adds a top-level menu entry with one submenu per tab.
*/
public function menu() {
$this->hook = add_menu_page(
__( 'M365 Login', 'm365-login' ),
__( 'M365 Login', 'm365-login' ),
'manage_options',
self::PAGE,
array( $this, 'render' ),
self::MENU_ICON,
81
);
foreach ( self::tabs() as $tab => $label ) {
add_submenu_page(
self::PAGE,
$label . ' – ' . __( 'M365 Login', 'm365-login' ),
$label,
'manage_options',
'connection' === $tab ? self::PAGE : 'admin.php?page=' . self::PAGE . '&tab=' . $tab
);
}
}
/**
* Tab slugs and labels.
*
* @return string[]
*/
public static function tabs() {
return array(
'connection' => __( 'Connection', 'm365-login' ),
'button' => __( 'Button', 'm365-login' ),
'security' => __( 'Security', 'm365-login' ),
);
}
/**
* Currently requested tab (from ?tab=).
*
* @return string
*/
private function current_tab() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only UI state.
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : '';
return array_key_exists( $tab, self::tabs() ) ? $tab : '';
}
/**
* Highlights the submenu entry matching the requested tab.
*
* @param string|null $submenu_file Current submenu file.
* @return string|null
*/
public function highlight_submenu( $submenu_file ) {
$screen = get_current_screen();
if ( ! $screen || $this->hook !== $screen->id ) {
return $submenu_file;
}
$tab = $this->current_tab();
if ( '' === $tab || 'connection' === $tab ) {
return self::PAGE;
}
return 'admin.php?page=' . self::PAGE . '&tab=' . $tab;
}
/**
* URL of the settings screen (optionally a specific tab).
*
* @param string $tab Tab slug.
* @return string
*/
public static function url( $tab = '' ) {
$url = admin_url( 'admin.php?page=' . self::PAGE );
return '' === $tab ? $url : add_query_arg( 'tab', $tab, $url );
}
/**
* 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(
'
',
esc_html__( 'M365 Login is active but not connected to Microsoft Entra ID yet.', 'm365-login' ),
esc_url( self::url() ),
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,
'certAction' => self::AJAX_CERT,
'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' ),
'generating' => __( 'Generating a 3072-bit key pair, this takes a moment…', 'm365-login' ),
'confirmCert' => __( 'Replace the stored certificate? Sign-in stops working until the new certificate is uploaded to Entra ID.', 'm365-login' ),
'confirmCertRemove' => __( 'Remove the stored certificate when saving? Sign-in with the certificate method 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 ) );
}
/**
* AJAX: generate a new self-signed certificate and store it (key encrypted).
*/
public function ajax_certificate() {
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 );
}
$op = isset( $_POST['op'] ) ? sanitize_key( wp_unslash( $_POST['op'] ) ) : '';
if ( 'generate' !== $op ) {
wp_send_json_error( array( 'message' => __( 'Unknown operation.', 'm365-login' ) ) );
}
$host = wp_parse_url( home_url(), PHP_URL_HOST );
$pair = M365_Login_Certificate::generate( is_string( $host ) ? $host : 'wordpress' );
if ( is_wp_error( $pair ) ) {
wp_send_json_error( array( 'message' => $pair->get_error_message() ) );
}
$stored = $this->settings->store_certificate( $pair );
if ( is_wp_error( $stored ) ) {
wp_send_json_error( array( 'message' => $stored->get_error_message() ) );
}
$this->graph->flush_token();
$info = M365_Login_Certificate::info( $pair['certificate'] );
wp_send_json_success(
array(
'message' => __( 'Certificate generated and stored. Download the .cer file and upload it in Entra ID.', 'm365-login' ),
'thumbprint' => $info ? $info['thumbprint'] : '',
)
);
}
/**
* Sends the public certificate as a .cer download (never the private key).
*/
public function download_certificate() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You are not allowed to do this.', 'm365-login' ), 403 );
}
check_admin_referer( self::POST_CERT );
$pem = $this->settings->certificate_pem();
if ( '' === $pem ) {
wp_die( esc_html__( 'No certificate is stored.', 'm365-login' ), 404 );
}
$host = wp_parse_url( home_url(), PHP_URL_HOST );
$name = 'm365-login-' . sanitize_file_name( is_string( $host ) ? $host : 'wordpress' ) . '.cer';
nocache_headers();
header( 'Content-Type: application/x-x509-ca-cert' );
header( 'Content-Disposition: attachment; filename="' . $name . '"' );
header( 'Content-Length: ' . strlen( $pem ) );
echo $pem; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- PEM text, public certificate only.
exit;
}
/**
* 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();
$method = $this->settings->auth_method();
$cert_info = $this->settings->certificate_info();
$option = M365_LOGIN_OPTION;
$field = function ( $key ) use ( $option ) {
return esc_attr( $option . '[' . $key . ']' );
};
?>