id="m365-button-only" />
@@ -495,7 +522,7 @@ class M365_Login_Admin {
-
+
settings->fallback_url() ) : ?>
settings->fallback_url() ); ?>
@@ -569,6 +596,7 @@ class M365_Login_Admin {
[m365_login_button redirect="/my-account/"]
+
diff --git a/includes/class-m365-login-auth.php b/includes/class-m365-login-auth.php
index 2efa8c6..9548087 100644
--- a/includes/class-m365-login-auth.php
+++ b/includes/class-m365-login-auth.php
@@ -51,10 +51,13 @@ class M365_Login_Auth {
add_action( 'init', array( $this, 'maybe_handle_callback' ), 5 );
add_filter( 'wp_login_errors', array( $this, 'login_errors' ), 10, 1 );
- // Button-only mode.
- add_action( 'login_init', array( $this, 'maybe_accept_fallback_key' ) );
+ // Button-only mode (works on wp-login.php and on custom login pages).
+ add_action( 'init', array( $this, 'maybe_accept_fallback_key' ), 6 );
// Runs after core's username/password handlers (priority 20), which would otherwise overwrite an early WP_Error.
add_filter( 'authenticate', array( $this, 'block_password_login' ), 99, 3 );
+
+ // Custom login page: send people back there after logging out.
+ add_filter( 'logout_redirect', array( $this, 'logout_redirect' ), 10, 3 );
}
/* ------------------------------------------------------------------ */
@@ -84,7 +87,7 @@ class M365_Login_Auth {
}
/**
- * wp-login.php?m365_fallback=KEY → sets the fallback cookie and reloads without the key in the URL.
+ * ?m365_fallback=KEY (on any page) → sets the fallback cookie and reloads the login page without the key in the URL.
*/
public function maybe_accept_fallback_key() {
if ( ! $this->settings->button_only() ) {
@@ -92,7 +95,7 @@ class M365_Login_Auth {
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the key itself is the secret.
$given = isset( $_GET['m365_fallback'] ) ? sanitize_text_field( wp_unslash( $_GET['m365_fallback'] ) ) : '';
- if ( '' === $given ) {
+ if ( '' === $given || 'on' === $given ) {
return;
}
@@ -111,10 +114,26 @@ class M365_Login_Auth {
delete_transient( $ip_key );
$this->send_cookie( self::FALLBACK_COOKIE, $this->fallback_cookie_value(), time() + self::FALLBACK_TTL );
nocache_headers();
- wp_safe_redirect( add_query_arg( 'm365_fallback', 'on', wp_login_url() ) );
+ wp_safe_redirect( add_query_arg( 'm365_fallback', 'on', $this->settings->login_page_url() ) );
exit;
}
+ /**
+ * After logout, return to the custom login page instead of wp-login.php.
+ *
+ * @param string $redirect_to Requested redirect.
+ * @param string $requested_redirect_to Raw requested redirect.
+ * @param WP_User|WP_Error $user User.
+ * @return string
+ */
+ public function logout_redirect( $redirect_to, $requested_redirect_to, $user ) {
+ $custom = $this->settings->custom_login_url();
+ if ( '' === $custom || '' !== (string) $requested_redirect_to ) {
+ return $redirect_to;
+ }
+ return add_query_arg( 'loggedout', 'true', $custom );
+ }
+
/**
* Refuses username/password sign-in on wp-login.php while button-only mode is active.
*
@@ -130,13 +149,27 @@ class M365_Login_Auth {
if ( '' === (string) $username && '' === (string) $password ) {
return $user; // Initial form render or cookie auth, no password attempt.
}
- // Only the interactive login form is affected: XML-RPC, REST and application passwords keep working.
- if ( ! isset( $GLOBALS['pagenow'] ) || 'wp-login.php' !== $GLOBALS['pagenow'] ) {
+ // Interactive password logins only: XML-RPC, REST (application passwords), WP-CLI and cron keep working.
+ if ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
+ || ( defined( 'REST_REQUEST' ) && REST_REQUEST )
+ || ( defined( 'WP_CLI' ) && WP_CLI )
+ || wp_doing_cron() ) {
return $user;
}
if ( ! $user instanceof WP_User ) {
return $user; // Already failed for another reason; keep core's message.
}
+
+ /**
+ * Allows exempting a password sign-in from button-only mode (e.g. a trusted membership plugin).
+ *
+ * @param bool $block Whether to block. Default true.
+ * @param WP_User $user Authenticated user.
+ */
+ if ( ! apply_filters( 'm365_login_block_password_login', true, $user ) ) {
+ return $user;
+ }
+
return new WP_Error( 'm365_login_button_only', __( 'Password sign-in is disabled on this site. Please use the Microsoft button.', 'm365-login' ) );
}
@@ -720,45 +753,48 @@ class M365_Login_Auth {
*/
private function fail( $code ) {
$this->clear_state_cookie();
- wp_safe_redirect( add_query_arg( 'm365_error', rawurlencode( $code ), wp_login_url() ) );
+ nocache_headers();
+ wp_safe_redirect( add_query_arg( 'm365_error', rawurlencode( $code ), $this->settings->login_page_url() ) );
exit;
}
/**
- * Writes to the PHP error log when WP_DEBUG_LOG is enabled.
+ * Messages for the current request (from ?m365_error and ?m365_fallback=on), for any login page.
*
- * @param string $message Message.
+ * @return array[] Each item: array( 'type' => 'error'|'message', 'code' => string, 'text' => string ).
*/
- private function log( $message ) {
- if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
- error_log( '[M365 Login] ' . $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
- }
- }
-
- /**
- * Maps error codes to messages on the login screen.
- *
- * @param WP_Error $errors Login errors.
- * @return WP_Error
- */
- public function login_errors( $errors ) {
- if ( ! $errors instanceof WP_Error ) {
- $errors = new WP_Error();
- }
-
+ public function current_messages() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- read-only display of whitelisted flags.
$code = isset( $_GET['m365_error'] ) ? sanitize_key( wp_unslash( $_GET['m365_error'] ) ) : '';
$fallback_on = isset( $_GET['m365_fallback'] ) && 'on' === $_GET['m365_fallback'];
// phpcs:enable WordPress.Security.NonceVerification.Recommended
+ $out = array();
if ( $fallback_on && $this->settings->button_only() && $this->fallback_active() ) {
- $errors->add( 'm365_login_fallback_on', __( 'Password sign-in is temporarily enabled for this browser (30 minutes).', 'm365-login' ), 'message' );
+ $out[] = array(
+ 'type' => 'message',
+ 'code' => 'fallback_on',
+ 'text' => __( 'Password sign-in is temporarily enabled for this browser (30 minutes).', 'm365-login' ),
+ );
}
- if ( '' === $code ) {
- return $errors;
+ if ( '' !== $code ) {
+ $messages = $this->error_messages();
+ $out[] = array(
+ 'type' => 'access_denied' === $code ? 'message' : 'error',
+ 'code' => $code,
+ 'text' => isset( $messages[ $code ] ) ? $messages[ $code ] : $messages['provider_error'],
+ );
}
+ return $out;
+ }
- $messages = array(
+ /**
+ * Error code → translated message map.
+ *
+ * @return string[]
+ */
+ private function error_messages() {
+ return array(
'not_configured' => __( 'Microsoft login is not configured yet.', 'm365-login' ),
'invalid_state' => __( 'The login request expired or was invalid. Please try again.', 'm365-login' ),
'access_denied' => __( 'Microsoft sign-in was cancelled.', 'm365-login' ),
@@ -775,13 +811,21 @@ class M365_Login_Auth {
'fallback_invalid' => __( 'The fallback key is not valid.', 'm365-login' ),
'fallback_locked' => __( 'Too many attempts. Please wait 15 minutes.', 'm365-login' ),
);
+ }
- $errors->add(
- 'm365_login_' . $code,
- isset( $messages[ $code ] ) ? $messages[ $code ] : $messages['provider_error'],
- 'access_denied' === $code ? 'message' : 'error'
- );
-
+ /**
+ * Adds the current messages to the wp-login.php error object.
+ *
+ * @param WP_Error $errors Login errors.
+ * @return WP_Error
+ */
+ public function login_errors( $errors ) {
+ if ( ! $errors instanceof WP_Error ) {
+ $errors = new WP_Error();
+ }
+ foreach ( $this->current_messages() as $msg ) {
+ $errors->add( 'm365_login_' . $msg['code'], $msg['text'], $msg['type'] );
+ }
return $errors;
}
}
diff --git a/includes/class-m365-login-button.php b/includes/class-m365-login-button.php
index 5ff81f8..ecae6fb 100644
--- a/includes/class-m365-login-button.php
+++ b/includes/class-m365-login-button.php
@@ -32,6 +32,133 @@ class M365_Login_Button {
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 .= '';
+ }
+ 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 .= '
';
+ }
+ $divider = $hidden ? '' : $this->divider_markup();
+ return $content . '' . $divider . $this->button( $redirect_to ) . '
';
+ }
+
+ /**
+ * 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 ) ? '' : '' . esc_html( $divider ) . '
';
+ }
+
+ /**
+ * Error / info messages for custom login pages.
+ *
+ * @return string
+ */
+ public function messages_markup() {
+ $html = '';
+ foreach ( M365_Login::instance()->auth->current_messages() as $msg ) {
+ $html .= '' . esc_html( $msg['text'] ) . '
';
+ }
+ 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 = '';
+ 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 . '
';
}
/**
@@ -142,15 +269,22 @@ class M365_Login_Button {
* @return string
*/
public function shortcode( $atts ) {
- if ( ! $this->settings->is_configured() || is_user_logged_in() ) {
- return '';
- }
- $atts = shortcode_atts( array( 'redirect' => '' ), $atts, 'm365_login_button' );
-
- 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() );
-
- return '' . $this->button( esc_url_raw( $atts['redirect'] ) ) . '
';
+ $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'] ),
+ )
+ );
}
/**
@@ -163,8 +297,7 @@ class M365_Login_Button {
// 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() ? '' : (string) $this->settings->get( 'divider_text' );
- $divider = '' === trim( $divider ) ? '' : '' . esc_html( $divider ) . '
';
+ $divider = $this->form_hidden() ? '' : $this->divider_markup();
$html = '';
$html .= 'above' === $position ? $this->button( $redirect_to ) . $divider : $divider . $this->button( $redirect_to );
diff --git a/includes/class-m365-login-settings.php b/includes/class-m365-login-settings.php
index bf78c22..e5bfc8c 100644
--- a/includes/class-m365-login-settings.php
+++ b/includes/class-m365-login-settings.php
@@ -51,6 +51,9 @@ class M365_Login_Settings {
'button_radius' => 4,
'button_position' => 'below',
'divider_text' => __( 'or', 'm365-login' ),
+ // Custom login pages.
+ 'custom_login_url' => '',
+ 'inject_form' => 1, // Add the button to wp_login_form() output.
);
}
@@ -209,7 +212,31 @@ class M365_Login_Settings {
*/
public function fallback_url() {
$key = $this->fallback_key();
- return '' === $key ? '' : add_query_arg( 'm365_fallback', $key, wp_login_url() );
+ return '' === $key ? '' : add_query_arg( 'm365_fallback', $key, $this->login_page_url() );
+ }
+
+ /**
+ * URL of a custom login page (same site), or empty string.
+ *
+ * @return string
+ */
+ public function custom_login_url() {
+ $url = (string) $this->get( 'custom_login_url', '' );
+ if ( '' === $url ) {
+ return '';
+ }
+ $validated = wp_validate_redirect( $url, '' );
+ return is_string( $validated ) ? $validated : '';
+ }
+
+ /**
+ * Where users land after a failed Microsoft sign-in (custom page or wp-login.php).
+ *
+ * @return string
+ */
+ public function login_page_url() {
+ $custom = $this->custom_login_url();
+ return '' !== $custom ? $custom : wp_login_url();
}
/**
@@ -309,6 +336,20 @@ class M365_Login_Settings {
}
$out['fallback_key'] = $key;
+ // Custom login page (must be on this site).
+ $custom = isset( $input['custom_login_url'] ) ? esc_url_raw( trim( wp_unslash( $input['custom_login_url'] ) ) ) : '';
+ if ( '' !== $custom ) {
+ if ( 0 === strpos( $custom, '/' ) ) {
+ $custom = home_url( $custom );
+ }
+ if ( '' === wp_validate_redirect( $custom, '' ) ) {
+ add_settings_error( M365_LOGIN_OPTION, 'custom_login_url', __( 'The custom login page must be a URL on this site.', 'm365-login' ) );
+ $custom = $current['custom_login_url'];
+ }
+ }
+ $out['custom_login_url'] = $custom;
+ $out['inject_form'] = empty( $input['inject_form'] ) ? 0 : 1;
+
// Button.
$text = isset( $input['button_text'] ) ? sanitize_text_field( wp_unslash( $input['button_text'] ) ) : '';
$out['button_text'] = '' === trim( $text ) ? $defaults['button_text'] : mb_substr( $text, 0, 80 );
diff --git a/includes/functions.php b/includes/functions.php
new file mode 100644
index 0000000..445999a
--- /dev/null
+++ b/includes/functions.php
@@ -0,0 +1,49 @@
+ home_url( '/dashboard/' ) ) ); ?>
+ *
+ * @param array $args {
+ * @type string $redirect Destination after login. Default: admin dashboard.
+ * @type bool $show_messages Show error/info messages from a previous attempt. Default true.
+ * @type bool $show_divider Show the "or" divider above the button. Default false.
+ * }
+ * @param bool $display Echo (true) or return (false). Default true.
+ * @return string
+ */
+ function m365_login_button( $args = array(), $display = true ) {
+ $plugin = M365_Login::instance();
+ $html = $plugin->button->standalone( $args );
+ if ( $display ) {
+ echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- markup is escaped where it is built.
+ }
+ return $html;
+ }
+}
+
+if ( ! function_exists( 'm365_login_messages' ) ) {
+ /**
+ * Outputs (or returns) the error/info messages of the last Microsoft sign-in attempt.
+ *
+ * @param bool $display Echo (true) or return (false). Default true.
+ * @return string
+ */
+ function m365_login_messages( $display = true ) {
+ $html = M365_Login::instance()->button->messages_markup();
+ if ( $display ) {
+ echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- markup is escaped where it is built.
+ }
+ return $html;
+ }
+}
diff --git a/languages/m365-login-de_DE.mo b/languages/m365-login-de_DE.mo
index e0634a8..66c78c1 100644
Binary files a/languages/m365-login-de_DE.mo and b/languages/m365-login-de_DE.mo differ
diff --git a/languages/m365-login-de_DE.po b/languages/m365-login-de_DE.po
index 1f61c79..c12aec7 100644
--- a/languages/m365-login-de_DE.po
+++ b/languages/m365-login-de_DE.po
@@ -40,7 +40,7 @@ msgstr "Dieses Icon verwenden"
msgid "Copied!"
msgstr "Kopiert!"
-#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:502 includes/class-m365-login-admin.php:545
+#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:529 includes/class-m365-login-admin.php:572
msgid "Copy"
msgstr "Kopieren"
@@ -64,11 +64,11 @@ msgstr "Suche läuft …"
msgid "Add"
msgstr "Hinzufügen"
-#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:476
+#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:503
msgid "Remove"
msgstr "Entfernen"
-#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:461
+#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:488
msgid "Save the connection settings first, then search for groups."
msgstr "Zuerst die Verbindungseinstellungen speichern, dann Gruppen suchen."
@@ -286,265 +286,309 @@ msgstr "Azure-Blau"
msgid "WordPress blue"
msgstr "WordPress-Blau"
+#: includes/class-m365-login-admin.php:412
+msgid "Custom login page"
+msgstr "Eigene Login-Seite"
+
+#: includes/class-m365-login-admin.php:413
+msgid "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."
+msgstr "Eigene Login-Seite statt wp-login.php? Hier eintragen, damit Fehlermeldungen, der Fallback-Link und die Weiterleitung nach dem Abmelden dorthin zeigen."
+
#: includes/class-m365-login-admin.php:416
+msgid "URL of your login page"
+msgstr "URL der Login-Seite"
+
+#: includes/class-m365-login-admin.php:418
+msgid "Must be on this site. Leave empty to use wp-login.php."
+msgstr "Muss auf dieser Website liegen. Leer lassen, um wp-login.php zu verwenden."
+
+#: includes/class-m365-login-admin.php:424
+msgid "Add the button to every wp_login_form() form automatically"
+msgstr "Button automatisch in jedes wp_login_form()-Formular einfügen"
+
+#: includes/class-m365-login-admin.php:425
+msgid "Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below."
+msgstr "Deckt Themes und Plugins ab, die die WordPress-Login-Formularfunktion verwenden. Page-Builder-Widgets benötigen den Shortcode oder die Template-Funktion unten."
+
+#: includes/class-m365-login-admin.php:430
+msgid "Manual placement"
+msgstr "Manuelle Platzierung"
+
+#: includes/class-m365-login-admin.php:431
+msgid "Shortcode (block editor, page builders):"
+msgstr "Shortcode (Block-Editor, Page Builder):"
+
+#: includes/class-m365-login-admin.php:433
+msgid "Template function (theme files):"
+msgstr "Template-Funktion (Theme-Dateien):"
+
+#: includes/class-m365-login-admin.php:435
+msgid "Both show the error messages of the last attempt; use m365_login_messages() to place them separately."
+msgstr "Beide zeigen die Fehlermeldungen des letzten Versuchs; mit m365_login_messages() lassen sie sich separat platzieren."
+
+#: includes/class-m365-login-admin.php:443
msgid "User matching & hardening"
msgstr "Benutzerzuordnung & Härtung"
-#: includes/class-m365-login-admin.php:417
+#: includes/class-m365-login-admin.php:444
msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
msgstr "Benutzer werden nie automatisch angelegt. Eine Microsoft-Anmeldung gelingt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert."
-#: includes/class-m365-login-admin.php:422
+#: includes/class-m365-login-admin.php:449
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden"
-#: includes/class-m365-login-admin.php:423
+#: includes/class-m365-login-admin.php:450
msgid "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."
msgstr "Bei der ersten Anmeldung wird die unveränderliche Microsoft-Objekt-ID am Benutzer gespeichert. Spätere Anmeldungen mit gleicher E-Mail, aber anderer Microsoft-Identität werden abgelehnt. Dringend empfohlen."
-#: includes/class-m365-login-admin.php:430
+#: includes/class-m365-login-admin.php:457
msgid "Fall back to the user principal name (UPN)"
msgstr "Auf den User Principal Name (UPN) zurückgreifen"
-#: includes/class-m365-login-admin.php:431
+#: includes/class-m365-login-admin.php:458
msgid "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."
msgstr "Enthält das Token keinen „email“-Claim, wird der UPN (z. B. jane@contoso.com) verwendet, sofern er eine gültige E-Mail-Adresse ist. Für Geschäftskonten meist erforderlich."
-#: includes/class-m365-login-admin.php:438
+#: includes/class-m365-login-admin.php:465
msgid "Keep users signed in (\"Remember me\")"
msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)"
-#: includes/class-m365-login-admin.php:439
+#: includes/class-m365-login-admin.php:466
msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung."
-#: includes/class-m365-login-admin.php:444
+#: includes/class-m365-login-admin.php:471
msgid "Allowed e-mail domains (optional)"
msgstr "Erlaubte E-Mail-Domains (optional)"
-#: includes/class-m365-login-admin.php:446
+#: includes/class-m365-login-admin.php:473
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr "Eine oder mehrere Domains, getrennt durch Kommas oder Zeilenumbrüche. Leer lassen, um alle Domains des Tenants zuzulassen."
-#: includes/class-m365-login-admin.php:451
+#: includes/class-m365-login-admin.php:478
msgid "Allowed Entra groups (optional)"
msgstr "Erlaubte Entra-Gruppen (optional)"
-#: includes/class-m365-login-admin.php:452
+#: includes/class-m365-login-admin.php:479
msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
msgstr "Nur Mitglieder mindestens einer dieser Gruppen dürfen sich anmelden. Leer lassen, um alle zugeordneten Benutzer zuzulassen. Verschachtelte Mitgliedschaften zählen."
-#: includes/class-m365-login-admin.php:455
+#: includes/class-m365-login-admin.php:482
msgid "Search groups"
msgstr "Gruppen suchen"
-#: includes/class-m365-login-admin.php:457
+#: includes/class-m365-login-admin.php:484
msgid "Type a group name or paste an object ID…"
msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
-#: includes/class-m365-login-admin.php:458
+#: includes/class-m365-login-admin.php:485
msgid "Search"
msgstr "Suchen"
-#: includes/class-m365-login-admin.php:463
+#: includes/class-m365-login-admin.php:490
msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
msgstr "Benötigt die Anwendungsberechtigung „GroupMember.Read.All“ mit Administratorzustimmung. Ohne sie können Gruppen-Objekt-IDs trotzdem eingefügt werden."
-#: includes/class-m365-login-admin.php:469
+#: includes/class-m365-login-admin.php:496
msgid "Selected groups"
msgstr "Ausgewählte Gruppen"
-#: includes/class-m365-login-admin.php:470
+#: includes/class-m365-login-admin.php:497
msgid "No groups selected – every matched user may sign in."
msgstr "Keine Gruppen ausgewählt – jeder zugeordnete Benutzer darf sich anmelden."
-#: includes/class-m365-login-admin.php:480
+#: includes/class-m365-login-admin.php:507
msgid "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."
msgstr "Die Mitgliedschaft wird aus dem „groups“-Claim des ID-Tokens gelesen, falls vorhanden; andernfalls fragt das Plugin Microsoft Graph (Anwendungsberechtigung „User.Read.All“ oder „Directory.Read.All“). Funktioniert beides nicht, wird die Anmeldung abgelehnt."
-#: includes/class-m365-login-admin.php:485
+#: includes/class-m365-login-admin.php:512
msgid "Button-only mode"
msgstr "Nur-Button-Modus"
-#: includes/class-m365-login-admin.php:486
-msgid "Hide the username/password form and the \"Lost your password?\" link, and refuse password sign-ins on the login page. Application passwords, REST and XML-RPC are not affected."
-msgstr "Blendet das Benutzername/Passwort-Formular und den Link „Passwort vergessen?“ aus und lehnt Passwort-Anmeldungen auf der Login-Seite ab. Anwendungspasswörter, REST und XML-RPC sind nicht betroffen."
+#: includes/class-m365-login-admin.php:513
+msgid "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."
+msgstr "Blendet die Benutzername/Passwort-Felder aus (auf wp-login.php und in wp_login_form()-Formularen) und lehnt jede interaktive Passwort-Anmeldung auf der Website ab, auch in eigenen Login-Formularen. Anwendungspasswörter, REST, XML-RPC und WP-CLI sind nicht betroffen."
-#: includes/class-m365-login-admin.php:491
+#: includes/class-m365-login-admin.php:518
msgid "Show only the Microsoft button on the login page"
msgstr "Auf der Login-Seite nur den Microsoft-Button anzeigen"
-#: includes/class-m365-login-admin.php:492
+#: includes/class-m365-login-admin.php:519
msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
msgstr "Wird aktiv, sobald die Verbindung eingerichtet ist. Vor dem Aktivieren sicherstellen, dass das eigene Konto sich per Microsoft anmelden kann."
-#: includes/class-m365-login-admin.php:497
+#: includes/class-m365-login-admin.php:524
msgid "Fallback link (keep it secret)"
msgstr "Fallback-Link (geheim halten)"
-#: includes/class-m365-login-admin.php:498
-msgid "Opening this link shows the password form again in that browser for 30 minutes. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks."
-msgstr "Wer diesen Link öffnet, sieht in diesem Browser 30 Minuten lang wieder das Passwort-Formular. Sicher aufbewahren – er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
+#: includes/class-m365-login-admin.php:525
+msgid "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."
+msgstr "Wer diesen Link öffnet, sieht in diesem Browser 30 Minuten lang wieder das Passwort-Formular und kann sich dort mit Passwort anmelden. Sicher aufbewahren – er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
-#: includes/class-m365-login-admin.php:506
+#: includes/class-m365-login-admin.php:533
msgid "Generate a new key when saving"
msgstr "Beim Speichern einen neuen Schlüssel erzeugen"
-#: includes/class-m365-login-admin.php:509
+#: includes/class-m365-login-admin.php:536
msgid "A key is generated automatically the first time you save these settings."
msgstr "Beim ersten Speichern dieser Einstellungen wird automatisch ein Schlüssel erzeugt."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:515
+#: includes/class-m365-login-admin.php:542
msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
msgstr "Notschalter: %s in die wp-config.php eintragen, um den Nur-Button-Modus vollständig abzuschalten."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:524
+#: includes/class-m365-login-admin.php:551
msgid "What the plugin does to keep sign-ins safe"
msgstr "So schützt das Plugin die Anmeldung"
-#: includes/class-m365-login-admin.php:526
+#: includes/class-m365-login-admin.php:553
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr "OpenID Connect Authorization Code Flow mit PKCE (S256) – Tokens laufen nie durch den Browser."
-#: includes/class-m365-login-admin.php:527
+#: includes/class-m365-login-admin.php:554
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr "Einmalige State- und Nonce-Werte, per HttpOnly-Cookie an den Browser gebunden (CSRF- und Replay-Schutz)."
-#: includes/class-m365-login-admin.php:528
+#: includes/class-m365-login-admin.php:555
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr "Signatur des ID-Tokens wird gegen Microsofts veröffentlichte Signaturschlüssel geprüft; Issuer, Audience, Tenant, Ablauf und Nonce werden kontrolliert."
-#: includes/class-m365-login-admin.php:529
+#: includes/class-m365-login-admin.php:556
msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
msgstr "Client Secret verschlüsselt gespeichert; es werden keine Konten angelegt und keine Passwörter geändert."
-#: includes/class-m365-login-admin.php:535
+#: includes/class-m365-login-admin.php:562
msgid "Save changes"
msgstr "Änderungen speichern"
-#: includes/class-m365-login-admin.php:541
+#: includes/class-m365-login-admin.php:568
msgid "Redirect URI"
msgstr "Umleitungs-URI (Redirect URI)"
-#: includes/class-m365-login-admin.php:542
+#: includes/class-m365-login-admin.php:569
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr "Diese URI in der App-Registrierung unter Authentifizierung → Web → Umleitungs-URIs eintragen:"
-#: includes/class-m365-login-admin.php:548
+#: includes/class-m365-login-admin.php:575
msgid "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."
msgstr "Einfache Permalinks sind aktiv, daher verwendet der Callback einen Query-String. Werden später sprechende Permalinks aktiviert, ändert sich die Umleitungs-URI und muss in Entra ID angepasst werden."
-#: includes/class-m365-login-admin.php:551
+#: includes/class-m365-login-admin.php:578
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr "Diese Website nutzt kein HTTPS. Microsoft akzeptiert http://-Umleitungs-URIs nur für localhost; produktive Websites benötigen HTTPS."
-#: includes/class-m365-login-admin.php:556
+#: includes/class-m365-login-admin.php:583
msgid "Setup in 5 steps"
msgstr "Einrichtung in 5 Schritten"
-#: includes/class-m365-login-admin.php:558
+#: includes/class-m365-login-admin.php:585
msgid "Open the Microsoft Entra admin center → App registrations → New registration."
msgstr "Microsoft Entra Admin Center öffnen → App-Registrierungen → Neue Registrierung."
-#: includes/class-m365-login-admin.php:559
+#: includes/class-m365-login-admin.php:586
msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above."
msgstr "„Nur Konten in diesem Organisationsverzeichnis“ wählen, Plattform „Web“ auswählen und die Umleitungs-URI von oben einfügen."
-#: includes/class-m365-login-admin.php:560
+#: includes/class-m365-login-admin.php:587
msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page."
msgstr "Anwendungs-ID (Client) und Verzeichnis-ID (Mandant) von der Übersichtsseite kopieren."
-#: includes/class-m365-login-admin.php:561
+#: includes/class-m365-login-admin.php:588
msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)."
msgstr "Unter „Zertifikate & Geheimnisse“ einen geheimen Clientschlüssel erstellen und dessen Wert (nicht die ID) kopieren."
-#: includes/class-m365-login-admin.php:562
+#: includes/class-m365-login-admin.php:589
msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page."
msgstr "Unter „Tokenkonfiguration“ den optionalen Anspruch „email“ für ID-Tokens hinzufügen (empfohlen), dann diese Seite speichern."
-#: includes/class-m365-login-admin.php:564
+#: includes/class-m365-login-admin.php:591
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) – standardmäßig vorhanden."
-#: includes/class-m365-login-admin.php:565
+#: includes/class-m365-login-admin.php:592
msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
msgstr "Optional für Gruppen-Beschränkungen: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung."
-#: includes/class-m365-login-admin.php:569
+#: includes/class-m365-login-admin.php:596
msgid "Shortcode"
msgstr "Shortcode"
-#: includes/class-m365-login-admin.php:570
+#: includes/class-m365-login-admin.php:597
msgid "Place the button on a custom login page:"
msgstr "Button auf einer eigenen Login-Seite platzieren:"
-#: includes/class-m365-login-auth.php:136
+#: includes/class-m365-login-admin.php:599
+msgid "More options on the Button tab under \"Custom login page\"."
+msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“."
+
+#: includes/class-m365-login-auth.php:173
msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
msgstr "Die Anmeldung mit Passwort ist auf dieser Website deaktiviert. Bitte den Microsoft-Button verwenden."
-#: includes/class-m365-login-auth.php:751
+#: includes/class-m365-login-auth.php:777
msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
msgstr "Die Passwort-Anmeldung ist für diesen Browser vorübergehend aktiviert (30 Minuten)."
-#: includes/class-m365-login-auth.php:758 includes/class-m365-login-graph.php:63
+#: includes/class-m365-login-auth.php:798 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet."
-#: includes/class-m365-login-auth.php:759
+#: includes/class-m365-login-auth.php:799
msgid "The login request expired or was invalid. Please try again."
msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:760
+#: includes/class-m365-login-auth.php:800
msgid "Microsoft sign-in was cancelled."
msgstr "Die Microsoft-Anmeldung wurde abgebrochen."
-#: includes/class-m365-login-auth.php:761
+#: includes/class-m365-login-auth.php:801
msgid "Microsoft returned an error. Please try again."
msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:762
+#: includes/class-m365-login-auth.php:802
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr "Die Anmeldung über Microsoft konnte nicht abgeschlossen werden. Bitte erneut versuchen oder einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:763
+#: includes/class-m365-login-auth.php:803
msgid "The Microsoft sign-in could not be verified."
msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden."
-#: includes/class-m365-login-auth.php:764
+#: includes/class-m365-login-auth.php:804
msgid "Your Microsoft account did not provide an e-mail address."
msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt."
-#: includes/class-m365-login-auth.php:765
+#: includes/class-m365-login-auth.php:805
msgid "Your e-mail domain is not allowed to sign in here."
msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen."
-#: includes/class-m365-login-auth.php:766
+#: includes/class-m365-login-auth.php:806
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr "Für die E-Mail-Adresse des Microsoft-Kontos existiert kein WordPress-Konto."
-#: includes/class-m365-login-auth.php:767
+#: includes/class-m365-login-auth.php:807
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr "Dieses WordPress-Konto ist mit einem anderen Microsoft-Konto verknüpft. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:768
+#: includes/class-m365-login-auth.php:808
msgid "You are not allowed to sign in with this account."
msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt."
-#: includes/class-m365-login-auth.php:769
+#: includes/class-m365-login-auth.php:809
msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
msgstr "Das Microsoft-Konto ist in keiner Gruppe, die sich hier anmelden darf."
-#: includes/class-m365-login-auth.php:770
+#: includes/class-m365-login-auth.php:810
msgid "Your group membership could not be verified. Please contact an administrator."
msgstr "Die Gruppenmitgliedschaft konnte nicht geprüft werden. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:771
+#: includes/class-m365-login-auth.php:811
msgid "The fallback key is not valid."
msgstr "Der Fallback-Schlüssel ist ungültig."
-#: includes/class-m365-login-auth.php:772
+#: includes/class-m365-login-auth.php:812
msgid "Too many attempts. Please wait 15 minutes."
msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
@@ -568,22 +612,26 @@ msgstr "Login mit Microsoft"
msgid "or"
msgstr "oder"
-#: includes/class-m365-login-settings.php:245
+#: includes/class-m365-login-settings.php:272
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr "Die Tenant-ID muss eine GUID (z. B. 1a2b3c4d-…) oder einer der Werte „organizations“, „common“, „consumers“ sein."
-#: includes/class-m365-login-settings.php:253
+#: includes/class-m365-login-settings.php:280
msgid "The application (client) ID must be a GUID."
msgstr "Die Anwendungs-ID (Client) muss eine GUID sein."
-#: includes/class-m365-login-settings.php:265
+#: includes/class-m365-login-settings.php:292
msgid "The client secret contains invalid characters."
msgstr "Das Client Secret enthält ungültige Zeichen."
-#: includes/class-m365-login-settings.php:269
+#: includes/class-m365-login-settings.php:296
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr "Das Client Secret konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
+#: includes/class-m365-login-settings.php:346
+msgid "The custom login page must be a URL on this site."
+msgstr "Die eigene Login-Seite muss eine URL dieser Website sein."
+
#: includes/class-m365-login.php:102
msgid "Settings"
msgstr "Einstellungen"
diff --git a/languages/m365-login-de_DE_formal.mo b/languages/m365-login-de_DE_formal.mo
index 4c79e3d..d83c951 100644
Binary files a/languages/m365-login-de_DE_formal.mo and b/languages/m365-login-de_DE_formal.mo differ
diff --git a/languages/m365-login-de_DE_formal.po b/languages/m365-login-de_DE_formal.po
index ed19b16..e16cd6e 100644
--- a/languages/m365-login-de_DE_formal.po
+++ b/languages/m365-login-de_DE_formal.po
@@ -40,7 +40,7 @@ msgstr "Dieses Icon verwenden"
msgid "Copied!"
msgstr "Kopiert!"
-#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:502 includes/class-m365-login-admin.php:545
+#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:529 includes/class-m365-login-admin.php:572
msgid "Copy"
msgstr "Kopieren"
@@ -64,11 +64,11 @@ msgstr "Suche läuft …"
msgid "Add"
msgstr "Hinzufügen"
-#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:476
+#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:503
msgid "Remove"
msgstr "Entfernen"
-#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:461
+#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:488
msgid "Save the connection settings first, then search for groups."
msgstr "Zuerst die Verbindungseinstellungen speichern, dann Gruppen suchen."
@@ -286,265 +286,309 @@ msgstr "Azure-Blau"
msgid "WordPress blue"
msgstr "WordPress-Blau"
+#: includes/class-m365-login-admin.php:412
+msgid "Custom login page"
+msgstr "Eigene Login-Seite"
+
+#: includes/class-m365-login-admin.php:413
+msgid "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."
+msgstr "Eigene Login-Seite statt wp-login.php? Hier eintragen, damit Fehlermeldungen, der Fallback-Link und die Weiterleitung nach dem Abmelden dorthin zeigen."
+
#: includes/class-m365-login-admin.php:416
+msgid "URL of your login page"
+msgstr "URL der Login-Seite"
+
+#: includes/class-m365-login-admin.php:418
+msgid "Must be on this site. Leave empty to use wp-login.php."
+msgstr "Muss auf dieser Website liegen. Leer lassen, um wp-login.php zu verwenden."
+
+#: includes/class-m365-login-admin.php:424
+msgid "Add the button to every wp_login_form() form automatically"
+msgstr "Button automatisch in jedes wp_login_form()-Formular einfügen"
+
+#: includes/class-m365-login-admin.php:425
+msgid "Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below."
+msgstr "Deckt Themes und Plugins ab, die die WordPress-Login-Formularfunktion verwenden. Page-Builder-Widgets benötigen den Shortcode oder die Template-Funktion unten."
+
+#: includes/class-m365-login-admin.php:430
+msgid "Manual placement"
+msgstr "Manuelle Platzierung"
+
+#: includes/class-m365-login-admin.php:431
+msgid "Shortcode (block editor, page builders):"
+msgstr "Shortcode (Block-Editor, Page Builder):"
+
+#: includes/class-m365-login-admin.php:433
+msgid "Template function (theme files):"
+msgstr "Template-Funktion (Theme-Dateien):"
+
+#: includes/class-m365-login-admin.php:435
+msgid "Both show the error messages of the last attempt; use m365_login_messages() to place them separately."
+msgstr "Beide zeigen die Fehlermeldungen des letzten Versuchs; mit m365_login_messages() lassen sie sich separat platzieren."
+
+#: includes/class-m365-login-admin.php:443
msgid "User matching & hardening"
msgstr "Benutzerzuordnung & Härtung"
-#: includes/class-m365-login-admin.php:417
+#: includes/class-m365-login-admin.php:444
msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
msgstr "Benutzer werden nie automatisch angelegt. Eine Microsoft-Anmeldung gelingt nur, wenn bereits ein WordPress-Benutzer mit derselben E-Mail-Adresse existiert."
-#: includes/class-m365-login-admin.php:422
+#: includes/class-m365-login-admin.php:449
msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr "WordPress-Konten an die Microsoft-Objekt-ID binden"
-#: includes/class-m365-login-admin.php:423
+#: includes/class-m365-login-admin.php:450
msgid "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."
msgstr "Bei der ersten Anmeldung wird die unveränderliche Microsoft-Objekt-ID am Benutzer gespeichert. Spätere Anmeldungen mit gleicher E-Mail, aber anderer Microsoft-Identität werden abgelehnt. Dringend empfohlen."
-#: includes/class-m365-login-admin.php:430
+#: includes/class-m365-login-admin.php:457
msgid "Fall back to the user principal name (UPN)"
msgstr "Auf den User Principal Name (UPN) zurückgreifen"
-#: includes/class-m365-login-admin.php:431
+#: includes/class-m365-login-admin.php:458
msgid "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."
msgstr "Enthält das Token keinen „email“-Claim, wird der UPN (z. B. jane@contoso.com) verwendet, sofern er eine gültige E-Mail-Adresse ist. Für Geschäftskonten meist erforderlich."
-#: includes/class-m365-login-admin.php:438
+#: includes/class-m365-login-admin.php:465
msgid "Keep users signed in (\"Remember me\")"
msgstr "Benutzer angemeldet lassen („Angemeldet bleiben“)"
-#: includes/class-m365-login-admin.php:439
+#: includes/class-m365-login-admin.php:466
msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr "Erstellt eine 14-tägige WordPress-Sitzung statt einer Browser-Sitzung."
-#: includes/class-m365-login-admin.php:444
+#: includes/class-m365-login-admin.php:471
msgid "Allowed e-mail domains (optional)"
msgstr "Erlaubte E-Mail-Domains (optional)"
-#: includes/class-m365-login-admin.php:446
+#: includes/class-m365-login-admin.php:473
msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
msgstr "Eine oder mehrere Domains, getrennt durch Kommas oder Zeilenumbrüche. Leer lassen, um alle Domains des Tenants zuzulassen."
-#: includes/class-m365-login-admin.php:451
+#: includes/class-m365-login-admin.php:478
msgid "Allowed Entra groups (optional)"
msgstr "Erlaubte Entra-Gruppen (optional)"
-#: includes/class-m365-login-admin.php:452
+#: includes/class-m365-login-admin.php:479
msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
msgstr "Nur Mitglieder mindestens einer dieser Gruppen dürfen sich anmelden. Leer lassen, um alle zugeordneten Benutzer zuzulassen. Verschachtelte Mitgliedschaften zählen."
-#: includes/class-m365-login-admin.php:455
+#: includes/class-m365-login-admin.php:482
msgid "Search groups"
msgstr "Gruppen suchen"
-#: includes/class-m365-login-admin.php:457
+#: includes/class-m365-login-admin.php:484
msgid "Type a group name or paste an object ID…"
msgstr "Gruppenname eingeben oder Objekt-ID einfügen …"
-#: includes/class-m365-login-admin.php:458
+#: includes/class-m365-login-admin.php:485
msgid "Search"
msgstr "Suchen"
-#: includes/class-m365-login-admin.php:463
+#: includes/class-m365-login-admin.php:490
msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
msgstr "Benötigt die Anwendungsberechtigung „GroupMember.Read.All“ mit Administratorzustimmung. Ohne sie können Gruppen-Objekt-IDs trotzdem eingefügt werden."
-#: includes/class-m365-login-admin.php:469
+#: includes/class-m365-login-admin.php:496
msgid "Selected groups"
msgstr "Ausgewählte Gruppen"
-#: includes/class-m365-login-admin.php:470
+#: includes/class-m365-login-admin.php:497
msgid "No groups selected – every matched user may sign in."
msgstr "Keine Gruppen ausgewählt – jeder zugeordnete Benutzer darf sich anmelden."
-#: includes/class-m365-login-admin.php:480
+#: includes/class-m365-login-admin.php:507
msgid "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."
msgstr "Die Mitgliedschaft wird aus dem „groups“-Claim des ID-Tokens gelesen, falls vorhanden; andernfalls fragt das Plugin Microsoft Graph (Anwendungsberechtigung „User.Read.All“ oder „Directory.Read.All“). Funktioniert beides nicht, wird die Anmeldung abgelehnt."
-#: includes/class-m365-login-admin.php:485
+#: includes/class-m365-login-admin.php:512
msgid "Button-only mode"
msgstr "Nur-Button-Modus"
-#: includes/class-m365-login-admin.php:486
-msgid "Hide the username/password form and the \"Lost your password?\" link, and refuse password sign-ins on the login page. Application passwords, REST and XML-RPC are not affected."
-msgstr "Blendet das Benutzername/Passwort-Formular und den Link „Passwort vergessen?“ aus und lehnt Passwort-Anmeldungen auf der Login-Seite ab. Anwendungspasswörter, REST und XML-RPC sind nicht betroffen."
+#: includes/class-m365-login-admin.php:513
+msgid "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."
+msgstr "Blendet die Benutzername/Passwort-Felder aus (auf wp-login.php und in wp_login_form()-Formularen) und lehnt jede interaktive Passwort-Anmeldung auf der Website ab, auch in eigenen Login-Formularen. Anwendungspasswörter, REST, XML-RPC und WP-CLI sind nicht betroffen."
-#: includes/class-m365-login-admin.php:491
+#: includes/class-m365-login-admin.php:518
msgid "Show only the Microsoft button on the login page"
msgstr "Auf der Login-Seite nur den Microsoft-Button anzeigen"
-#: includes/class-m365-login-admin.php:492
+#: includes/class-m365-login-admin.php:519
msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
msgstr "Wird aktiv, sobald die Verbindung eingerichtet ist. Vor dem Aktivieren sicherstellen, dass das eigene Konto sich per Microsoft anmelden kann."
-#: includes/class-m365-login-admin.php:497
+#: includes/class-m365-login-admin.php:524
msgid "Fallback link (keep it secret)"
msgstr "Fallback-Link (geheim halten)"
-#: includes/class-m365-login-admin.php:498
-msgid "Opening this link shows the password form again in that browser for 30 minutes. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks."
-msgstr "Wer diesen Link öffnet, sieht in diesem Browser 30 Minuten lang wieder das Passwort-Formular. Sicher aufbewahren – er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
+#: includes/class-m365-login-admin.php:525
+msgid "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."
+msgstr "Wer diesen Link öffnet, sieht in diesem Browser 30 Minuten lang wieder das Passwort-Formular und kann sich dort mit Passwort anmelden. Sicher aufbewahren – er ist der Weg zurück, falls die Microsoft-Anmeldung einmal nicht funktioniert."
-#: includes/class-m365-login-admin.php:506
+#: includes/class-m365-login-admin.php:533
msgid "Generate a new key when saving"
msgstr "Beim Speichern einen neuen Schlüssel erzeugen"
-#: includes/class-m365-login-admin.php:509
+#: includes/class-m365-login-admin.php:536
msgid "A key is generated automatically the first time you save these settings."
msgstr "Beim ersten Speichern dieser Einstellungen wird automatisch ein Schlüssel erzeugt."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:515
+#: includes/class-m365-login-admin.php:542
msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
msgstr "Notschalter: %s in die wp-config.php eintragen, um den Nur-Button-Modus vollständig abzuschalten."
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:524
+#: includes/class-m365-login-admin.php:551
msgid "What the plugin does to keep sign-ins safe"
msgstr "So schützt das Plugin die Anmeldung"
-#: includes/class-m365-login-admin.php:526
+#: includes/class-m365-login-admin.php:553
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr "OpenID Connect Authorization Code Flow mit PKCE (S256) – Tokens laufen nie durch den Browser."
-#: includes/class-m365-login-admin.php:527
+#: includes/class-m365-login-admin.php:554
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr "Einmalige State- und Nonce-Werte, per HttpOnly-Cookie an den Browser gebunden (CSRF- und Replay-Schutz)."
-#: includes/class-m365-login-admin.php:528
+#: includes/class-m365-login-admin.php:555
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr "Signatur des ID-Tokens wird gegen Microsofts veröffentlichte Signaturschlüssel geprüft; Issuer, Audience, Tenant, Ablauf und Nonce werden kontrolliert."
-#: includes/class-m365-login-admin.php:529
+#: includes/class-m365-login-admin.php:556
msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
msgstr "Client Secret verschlüsselt gespeichert; es werden keine Konten angelegt und keine Passwörter geändert."
-#: includes/class-m365-login-admin.php:535
+#: includes/class-m365-login-admin.php:562
msgid "Save changes"
msgstr "Änderungen speichern"
-#: includes/class-m365-login-admin.php:541
+#: includes/class-m365-login-admin.php:568
msgid "Redirect URI"
msgstr "Umleitungs-URI (Redirect URI)"
-#: includes/class-m365-login-admin.php:542
+#: includes/class-m365-login-admin.php:569
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr "Diese URI in der App-Registrierung unter Authentifizierung → Web → Umleitungs-URIs eintragen:"
-#: includes/class-m365-login-admin.php:548
+#: includes/class-m365-login-admin.php:575
msgid "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."
msgstr "Einfache Permalinks sind aktiv, daher verwendet der Callback einen Query-String. Werden später sprechende Permalinks aktiviert, ändert sich die Umleitungs-URI und muss in Entra ID angepasst werden."
-#: includes/class-m365-login-admin.php:551
+#: includes/class-m365-login-admin.php:578
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr "Diese Website nutzt kein HTTPS. Microsoft akzeptiert http://-Umleitungs-URIs nur für localhost; produktive Websites benötigen HTTPS."
-#: includes/class-m365-login-admin.php:556
+#: includes/class-m365-login-admin.php:583
msgid "Setup in 5 steps"
msgstr "Einrichtung in 5 Schritten"
-#: includes/class-m365-login-admin.php:558
+#: includes/class-m365-login-admin.php:585
msgid "Open the Microsoft Entra admin center → App registrations → New registration."
msgstr "Microsoft Entra Admin Center öffnen → App-Registrierungen → Neue Registrierung."
-#: includes/class-m365-login-admin.php:559
+#: includes/class-m365-login-admin.php:586
msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above."
msgstr "„Nur Konten in diesem Organisationsverzeichnis“ wählen, Plattform „Web“ auswählen und die Umleitungs-URI von oben einfügen."
-#: includes/class-m365-login-admin.php:560
+#: includes/class-m365-login-admin.php:587
msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page."
msgstr "Anwendungs-ID (Client) und Verzeichnis-ID (Mandant) von der Übersichtsseite kopieren."
-#: includes/class-m365-login-admin.php:561
+#: includes/class-m365-login-admin.php:588
msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)."
msgstr "Unter „Zertifikate & Geheimnisse“ einen geheimen Clientschlüssel erstellen und dessen Wert (nicht die ID) kopieren."
-#: includes/class-m365-login-admin.php:562
+#: includes/class-m365-login-admin.php:589
msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page."
msgstr "Unter „Tokenkonfiguration“ den optionalen Anspruch „email“ für ID-Tokens hinzufügen (empfohlen), dann diese Seite speichern."
-#: includes/class-m365-login-admin.php:564
+#: includes/class-m365-login-admin.php:591
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr "Benötigte API-Berechtigungen: openid, profile, email (delegiert) – standardmäßig vorhanden."
-#: includes/class-m365-login-admin.php:565
+#: includes/class-m365-login-admin.php:592
msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
msgstr "Optional für Gruppen-Beschränkungen: Anwendungsberechtigungen GroupMember.Read.All und User.Read.All (Microsoft Graph) mit Administratorzustimmung."
-#: includes/class-m365-login-admin.php:569
+#: includes/class-m365-login-admin.php:596
msgid "Shortcode"
msgstr "Shortcode"
-#: includes/class-m365-login-admin.php:570
+#: includes/class-m365-login-admin.php:597
msgid "Place the button on a custom login page:"
msgstr "Button auf einer eigenen Login-Seite platzieren:"
-#: includes/class-m365-login-auth.php:136
+#: includes/class-m365-login-admin.php:599
+msgid "More options on the Button tab under \"Custom login page\"."
+msgstr "Weitere Optionen im Tab „Button“ unter „Eigene Login-Seite“."
+
+#: includes/class-m365-login-auth.php:173
msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
msgstr "Die Anmeldung mit Passwort ist auf dieser Website deaktiviert. Bitte den Microsoft-Button verwenden."
-#: includes/class-m365-login-auth.php:751
+#: includes/class-m365-login-auth.php:777
msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
msgstr "Die Passwort-Anmeldung ist für diesen Browser vorübergehend aktiviert (30 Minuten)."
-#: includes/class-m365-login-auth.php:758 includes/class-m365-login-graph.php:63
+#: includes/class-m365-login-auth.php:798 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr "Die Microsoft-Anmeldung ist noch nicht eingerichtet."
-#: includes/class-m365-login-auth.php:759
+#: includes/class-m365-login-auth.php:799
msgid "The login request expired or was invalid. Please try again."
msgstr "Die Anmeldeanfrage ist abgelaufen oder ungültig. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:760
+#: includes/class-m365-login-auth.php:800
msgid "Microsoft sign-in was cancelled."
msgstr "Die Microsoft-Anmeldung wurde abgebrochen."
-#: includes/class-m365-login-auth.php:761
+#: includes/class-m365-login-auth.php:801
msgid "Microsoft returned an error. Please try again."
msgstr "Microsoft hat einen Fehler gemeldet. Bitte erneut versuchen."
-#: includes/class-m365-login-auth.php:762
+#: includes/class-m365-login-auth.php:802
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr "Die Anmeldung über Microsoft konnte nicht abgeschlossen werden. Bitte erneut versuchen oder einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:763
+#: includes/class-m365-login-auth.php:803
msgid "The Microsoft sign-in could not be verified."
msgstr "Die Microsoft-Anmeldung konnte nicht verifiziert werden."
-#: includes/class-m365-login-auth.php:764
+#: includes/class-m365-login-auth.php:804
msgid "Your Microsoft account did not provide an e-mail address."
msgstr "Das Microsoft-Konto hat keine E-Mail-Adresse übermittelt."
-#: includes/class-m365-login-auth.php:765
+#: includes/class-m365-login-auth.php:805
msgid "Your e-mail domain is not allowed to sign in here."
msgstr "Diese E-Mail-Domain ist hier nicht zur Anmeldung zugelassen."
-#: includes/class-m365-login-auth.php:766
+#: includes/class-m365-login-auth.php:806
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr "Für die E-Mail-Adresse des Microsoft-Kontos existiert kein WordPress-Konto."
-#: includes/class-m365-login-auth.php:767
+#: includes/class-m365-login-auth.php:807
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr "Dieses WordPress-Konto ist mit einem anderen Microsoft-Konto verknüpft. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:768
+#: includes/class-m365-login-auth.php:808
msgid "You are not allowed to sign in with this account."
msgstr "Die Anmeldung mit diesem Konto ist nicht erlaubt."
-#: includes/class-m365-login-auth.php:769
+#: includes/class-m365-login-auth.php:809
msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
msgstr "Das Microsoft-Konto ist in keiner Gruppe, die sich hier anmelden darf."
-#: includes/class-m365-login-auth.php:770
+#: includes/class-m365-login-auth.php:810
msgid "Your group membership could not be verified. Please contact an administrator."
msgstr "Die Gruppenmitgliedschaft konnte nicht geprüft werden. Bitte einen Administrator kontaktieren."
-#: includes/class-m365-login-auth.php:771
+#: includes/class-m365-login-auth.php:811
msgid "The fallback key is not valid."
msgstr "Der Fallback-Schlüssel ist ungültig."
-#: includes/class-m365-login-auth.php:772
+#: includes/class-m365-login-auth.php:812
msgid "Too many attempts. Please wait 15 minutes."
msgstr "Zu viele Versuche. Bitte 15 Minuten warten."
@@ -568,22 +612,26 @@ msgstr "Login mit Microsoft"
msgid "or"
msgstr "oder"
-#: includes/class-m365-login-settings.php:245
+#: includes/class-m365-login-settings.php:272
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr "Die Tenant-ID muss eine GUID (z. B. 1a2b3c4d-…) oder einer der Werte „organizations“, „common“, „consumers“ sein."
-#: includes/class-m365-login-settings.php:253
+#: includes/class-m365-login-settings.php:280
msgid "The application (client) ID must be a GUID."
msgstr "Die Anwendungs-ID (Client) muss eine GUID sein."
-#: includes/class-m365-login-settings.php:265
+#: includes/class-m365-login-settings.php:292
msgid "The client secret contains invalid characters."
msgstr "Das Client Secret enthält ungültige Zeichen."
-#: includes/class-m365-login-settings.php:269
+#: includes/class-m365-login-settings.php:296
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr "Das Client Secret konnte nicht verschlüsselt werden. Ist die OpenSSL-Erweiterung verfügbar?"
+#: includes/class-m365-login-settings.php:346
+msgid "The custom login page must be a URL on this site."
+msgstr "Die eigene Login-Seite muss eine URL dieser Website sein."
+
#: includes/class-m365-login.php:102
msgid "Settings"
msgstr "Einstellungen"
diff --git a/languages/m365-login.pot b/languages/m365-login.pot
index b5bce91..512e6a3 100644
--- a/languages/m365-login.pot
+++ b/languages/m365-login.pot
@@ -38,7 +38,7 @@ msgstr ""
msgid "Copied!"
msgstr ""
-#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:502 includes/class-m365-login-admin.php:545
+#: includes/class-m365-login-admin.php:148 includes/class-m365-login-admin.php:529 includes/class-m365-login-admin.php:572
msgid "Copy"
msgstr ""
@@ -62,11 +62,11 @@ msgstr ""
msgid "Add"
msgstr ""
-#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:476
+#: includes/class-m365-login-admin.php:154 includes/class-m365-login-admin.php:503
msgid "Remove"
msgstr ""
-#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:461
+#: includes/class-m365-login-admin.php:155 includes/class-m365-login-admin.php:210 includes/class-m365-login-admin.php:488
msgid "Save the connection settings first, then search for groups."
msgstr ""
@@ -284,265 +284,309 @@ msgstr ""
msgid "WordPress blue"
msgstr ""
+#: includes/class-m365-login-admin.php:412
+msgid "Custom login page"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:413
+msgid "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."
+msgstr ""
+
#: includes/class-m365-login-admin.php:416
-msgid "User matching & hardening"
+msgid "URL of your login page"
msgstr ""
-#: includes/class-m365-login-admin.php:417
-msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
+#: includes/class-m365-login-admin.php:418
+msgid "Must be on this site. Leave empty to use wp-login.php."
msgstr ""
-#: includes/class-m365-login-admin.php:422
-msgid "Bind WordPress accounts to the Microsoft object ID"
+#: includes/class-m365-login-admin.php:424
+msgid "Add the button to every wp_login_form() form automatically"
msgstr ""
-#: includes/class-m365-login-admin.php:423
-msgid "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."
+#: includes/class-m365-login-admin.php:425
+msgid "Covers themes and plugins that use the WordPress login form function. Page-builder widgets need the shortcode or the template function below."
msgstr ""
#: includes/class-m365-login-admin.php:430
-msgid "Fall back to the user principal name (UPN)"
+msgid "Manual placement"
msgstr ""
#: includes/class-m365-login-admin.php:431
-msgid "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."
+msgid "Shortcode (block editor, page builders):"
msgstr ""
-#: includes/class-m365-login-admin.php:438
-msgid "Keep users signed in (\"Remember me\")"
+#: includes/class-m365-login-admin.php:433
+msgid "Template function (theme files):"
msgstr ""
-#: includes/class-m365-login-admin.php:439
-msgid "Issues a 14-day WordPress session instead of a browser session."
+#: includes/class-m365-login-admin.php:435
+msgid "Both show the error messages of the last attempt; use m365_login_messages() to place them separately."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:443
+msgid "User matching & hardening"
msgstr ""
#: includes/class-m365-login-admin.php:444
-msgid "Allowed e-mail domains (optional)"
+msgid "Users are never created automatically. A Microsoft sign-in only succeeds when a WordPress user with the same e-mail address already exists."
msgstr ""
-#: includes/class-m365-login-admin.php:446
-msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
+#: includes/class-m365-login-admin.php:449
+msgid "Bind WordPress accounts to the Microsoft object ID"
msgstr ""
-#: includes/class-m365-login-admin.php:451
-msgid "Allowed Entra groups (optional)"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:452
-msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
-msgstr ""
-
-#: includes/class-m365-login-admin.php:455
-msgid "Search groups"
+#: includes/class-m365-login-admin.php:450
+msgid "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."
msgstr ""
#: includes/class-m365-login-admin.php:457
-msgid "Type a group name or paste an object ID…"
+msgid "Fall back to the user principal name (UPN)"
msgstr ""
#: includes/class-m365-login-admin.php:458
-msgid "Search"
+msgid "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."
msgstr ""
-#: includes/class-m365-login-admin.php:463
-msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
+#: includes/class-m365-login-admin.php:465
+msgid "Keep users signed in (\"Remember me\")"
msgstr ""
-#: includes/class-m365-login-admin.php:469
-msgid "Selected groups"
+#: includes/class-m365-login-admin.php:466
+msgid "Issues a 14-day WordPress session instead of a browser session."
msgstr ""
-#: includes/class-m365-login-admin.php:470
-msgid "No groups selected – every matched user may sign in."
+#: includes/class-m365-login-admin.php:471
+msgid "Allowed e-mail domains (optional)"
msgstr ""
-#: includes/class-m365-login-admin.php:480
-msgid "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."
+#: includes/class-m365-login-admin.php:473
+msgid "One or more domains separated by commas or new lines. Leave empty to allow any domain of your tenant."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:478
+msgid "Allowed Entra groups (optional)"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:479
+msgid "Only members of at least one of these groups may sign in. Leave empty to allow every matched user. Nested memberships count."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:482
+msgid "Search groups"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:484
+msgid "Type a group name or paste an object ID…"
msgstr ""
#: includes/class-m365-login-admin.php:485
-msgid "Button-only mode"
+msgid "Search"
msgstr ""
-#: includes/class-m365-login-admin.php:486
-msgid "Hide the username/password form and the \"Lost your password?\" link, and refuse password sign-ins on the login page. Application passwords, REST and XML-RPC are not affected."
+#: includes/class-m365-login-admin.php:490
+msgid "Needs the application permission \"GroupMember.Read.All\" with admin consent. Without it you can still paste group object IDs."
msgstr ""
-#: includes/class-m365-login-admin.php:491
-msgid "Show only the Microsoft button on the login page"
-msgstr ""
-
-#: includes/class-m365-login-admin.php:492
-msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
+#: includes/class-m365-login-admin.php:496
+msgid "Selected groups"
msgstr ""
#: includes/class-m365-login-admin.php:497
+msgid "No groups selected – every matched user may sign in."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:507
+msgid "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."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:512
+msgid "Button-only mode"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:513
+msgid "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."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:518
+msgid "Show only the Microsoft button on the login page"
+msgstr ""
+
+#: includes/class-m365-login-admin.php:519
+msgid "Becomes active once the connection is configured. Make sure your own account can sign in via Microsoft before enabling this."
+msgstr ""
+
+#: includes/class-m365-login-admin.php:524
msgid "Fallback link (keep it secret)"
msgstr ""
-#: includes/class-m365-login-admin.php:498
-msgid "Opening this link shows the password form again in that browser for 30 minutes. Bookmark it somewhere safe – it is your way back in if Microsoft sign-in ever breaks."
+#: includes/class-m365-login-admin.php:525
+msgid "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."
msgstr ""
-#: includes/class-m365-login-admin.php:506
+#: includes/class-m365-login-admin.php:533
msgid "Generate a new key when saving"
msgstr ""
-#: includes/class-m365-login-admin.php:509
+#: includes/class-m365-login-admin.php:536
msgid "A key is generated automatically the first time you save these settings."
msgstr ""
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:515
+#: includes/class-m365-login-admin.php:542
msgid "Emergency switch: add %s to wp-config.php to disable button-only mode entirely."
msgstr ""
#. translators: %s: PHP constant
-#: includes/class-m365-login-admin.php:524
+#: includes/class-m365-login-admin.php:551
msgid "What the plugin does to keep sign-ins safe"
msgstr ""
-#: includes/class-m365-login-admin.php:526
+#: includes/class-m365-login-admin.php:553
msgid "OpenID Connect authorization code flow with PKCE (S256) – no tokens ever pass through the browser."
msgstr ""
-#: includes/class-m365-login-admin.php:527
+#: includes/class-m365-login-admin.php:554
msgid "Single-use state and nonce values bound to the browser via an HttpOnly cookie (CSRF and replay protection)."
msgstr ""
-#: includes/class-m365-login-admin.php:528
+#: includes/class-m365-login-admin.php:555
msgid "ID token signature verified against Microsoft’s published signing keys; issuer, audience, tenant, expiry and nonce are checked."
msgstr ""
-#: includes/class-m365-login-admin.php:529
+#: includes/class-m365-login-admin.php:556
msgid "Client secret encrypted at rest; no accounts are created, no passwords are changed."
msgstr ""
-#: includes/class-m365-login-admin.php:535
+#: includes/class-m365-login-admin.php:562
msgid "Save changes"
msgstr ""
-#: includes/class-m365-login-admin.php:541
+#: includes/class-m365-login-admin.php:568
msgid "Redirect URI"
msgstr ""
-#: includes/class-m365-login-admin.php:542
+#: includes/class-m365-login-admin.php:569
msgid "Register this URI in your app registration under Authentication → Web → Redirect URIs:"
msgstr ""
-#: includes/class-m365-login-admin.php:548
+#: includes/class-m365-login-admin.php:575
msgid "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."
msgstr ""
-#: includes/class-m365-login-admin.php:551
+#: includes/class-m365-login-admin.php:578
msgid "Your site does not use HTTPS. Microsoft only accepts http:// redirect URIs for localhost; production sites must use HTTPS."
msgstr ""
-#: includes/class-m365-login-admin.php:556
+#: includes/class-m365-login-admin.php:583
msgid "Setup in 5 steps"
msgstr ""
-#: includes/class-m365-login-admin.php:558
+#: includes/class-m365-login-admin.php:585
msgid "Open the Microsoft Entra admin center → App registrations → New registration."
msgstr ""
-#: includes/class-m365-login-admin.php:559
+#: includes/class-m365-login-admin.php:586
msgid "Choose \"Accounts in this organizational directory only\", set the platform to Web and paste the redirect URI above."
msgstr ""
-#: includes/class-m365-login-admin.php:560
+#: includes/class-m365-login-admin.php:587
msgid "Copy the Application (client) ID and Directory (tenant) ID from the overview page."
msgstr ""
-#: includes/class-m365-login-admin.php:561
+#: includes/class-m365-login-admin.php:588
msgid "Under Certificates & secrets create a client secret and copy its value (not the ID)."
msgstr ""
-#: includes/class-m365-login-admin.php:562
+#: includes/class-m365-login-admin.php:589
msgid "Under Token configuration add the optional claim \"email\" for ID tokens (recommended), then save this page."
msgstr ""
-#: includes/class-m365-login-admin.php:564
+#: includes/class-m365-login-admin.php:591
msgid "Required API permission: openid, profile, email (delegated) – granted by default."
msgstr ""
-#: includes/class-m365-login-admin.php:565
+#: includes/class-m365-login-admin.php:592
msgid "Optional, for group restrictions: application permissions GroupMember.Read.All and User.Read.All (Microsoft Graph) with admin consent."
msgstr ""
-#: includes/class-m365-login-admin.php:569
+#: includes/class-m365-login-admin.php:596
msgid "Shortcode"
msgstr ""
-#: includes/class-m365-login-admin.php:570
+#: includes/class-m365-login-admin.php:597
msgid "Place the button on a custom login page:"
msgstr ""
-#: includes/class-m365-login-auth.php:136
+#: includes/class-m365-login-admin.php:599
+msgid "More options on the Button tab under \"Custom login page\"."
+msgstr ""
+
+#: includes/class-m365-login-auth.php:173
msgid "Password sign-in is disabled on this site. Please use the Microsoft button."
msgstr ""
-#: includes/class-m365-login-auth.php:751
+#: includes/class-m365-login-auth.php:777
msgid "Password sign-in is temporarily enabled for this browser (30 minutes)."
msgstr ""
-#: includes/class-m365-login-auth.php:758 includes/class-m365-login-graph.php:63
+#: includes/class-m365-login-auth.php:798 includes/class-m365-login-graph.php:63
msgid "Microsoft login is not configured yet."
msgstr ""
-#: includes/class-m365-login-auth.php:759
+#: includes/class-m365-login-auth.php:799
msgid "The login request expired or was invalid. Please try again."
msgstr ""
-#: includes/class-m365-login-auth.php:760
+#: includes/class-m365-login-auth.php:800
msgid "Microsoft sign-in was cancelled."
msgstr ""
-#: includes/class-m365-login-auth.php:761
+#: includes/class-m365-login-auth.php:801
msgid "Microsoft returned an error. Please try again."
msgstr ""
-#: includes/class-m365-login-auth.php:762
+#: includes/class-m365-login-auth.php:802
msgid "Could not complete the sign-in with Microsoft. Please try again or contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:763
+#: includes/class-m365-login-auth.php:803
msgid "The Microsoft sign-in could not be verified."
msgstr ""
-#: includes/class-m365-login-auth.php:764
+#: includes/class-m365-login-auth.php:804
msgid "Your Microsoft account did not provide an e-mail address."
msgstr ""
-#: includes/class-m365-login-auth.php:765
+#: includes/class-m365-login-auth.php:805
msgid "Your e-mail domain is not allowed to sign in here."
msgstr ""
-#: includes/class-m365-login-auth.php:766
+#: includes/class-m365-login-auth.php:806
msgid "No WordPress account exists for your Microsoft e-mail address."
msgstr ""
-#: includes/class-m365-login-auth.php:767
+#: includes/class-m365-login-auth.php:807
msgid "This WordPress account is linked to a different Microsoft account. Please contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:768
+#: includes/class-m365-login-auth.php:808
msgid "You are not allowed to sign in with this account."
msgstr ""
-#: includes/class-m365-login-auth.php:769
+#: includes/class-m365-login-auth.php:809
msgid "Your Microsoft account is not a member of a group that is allowed to sign in here."
msgstr ""
-#: includes/class-m365-login-auth.php:770
+#: includes/class-m365-login-auth.php:810
msgid "Your group membership could not be verified. Please contact an administrator."
msgstr ""
-#: includes/class-m365-login-auth.php:771
+#: includes/class-m365-login-auth.php:811
msgid "The fallback key is not valid."
msgstr ""
-#: includes/class-m365-login-auth.php:772
+#: includes/class-m365-login-auth.php:812
msgid "Too many attempts. Please wait 15 minutes."
msgstr ""
@@ -566,22 +610,26 @@ msgstr ""
msgid "or"
msgstr ""
-#: includes/class-m365-login-settings.php:245
+#: includes/class-m365-login-settings.php:272
msgid "The tenant ID must be a GUID (e.g. 1a2b3c4d-…) or one of \"organizations\", \"common\", \"consumers\"."
msgstr ""
-#: includes/class-m365-login-settings.php:253
+#: includes/class-m365-login-settings.php:280
msgid "The application (client) ID must be a GUID."
msgstr ""
-#: includes/class-m365-login-settings.php:265
+#: includes/class-m365-login-settings.php:292
msgid "The client secret contains invalid characters."
msgstr ""
-#: includes/class-m365-login-settings.php:269
+#: includes/class-m365-login-settings.php:296
msgid "The client secret could not be encrypted. Is the OpenSSL extension available?"
msgstr ""
+#: includes/class-m365-login-settings.php:346
+msgid "The custom login page must be a URL on this site."
+msgstr ""
+
#: includes/class-m365-login.php:102
msgid "Settings"
msgstr ""
diff --git a/m365-login.php b/m365-login.php
index 190d698..8713c46 100644
--- a/m365-login.php
+++ b/m365-login.php
@@ -30,6 +30,7 @@ require_once M365_LOGIN_DIR . 'includes/class-m365-login-auth.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-button.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login-admin.php';
require_once M365_LOGIN_DIR . 'includes/class-m365-login.php';
+require_once M365_LOGIN_DIR . 'includes/functions.php';
register_activation_hook( __FILE__, array( 'M365_Login', 'activate' ) );
diff --git a/readme.txt b/readme.txt
index 3b4a65b..faa945b 100644
--- a/readme.txt
+++ b/readme.txt
@@ -22,7 +22,7 @@ The plugin is deliberately small and strict:
* **Entra group restriction.** Search and pick the groups whose members may sign in, right in the settings screen. Membership is checked via the ID token's `groups` claim or Microsoft Graph (nested groups included).
* **Button-only mode.** Hide the username/password form and refuse password sign-ins on the login page. A secret fallback link (and a `wp-config.php` constant) brings the form back when you need it.
* **Clean settings screen** with a copy-and-paste redirect URI, a tenant connectivity test and a five-step setup guide.
-* **Shortcode** `[m365_login_button]` for custom login pages.
+* **Custom login pages.** The button is added to every `wp_login_form()` form automatically; a shortcode and a template function cover page builders and theme templates. Point the plugin at your login page and error messages, the fallback link and the post-logout redirect go there instead of wp-login.php.
= Security =
@@ -43,6 +43,7 @@ The plugin is deliberately small and strict:
* `m365_login_match_email` – filter the e-mail address used for the lookup.
* `m365_login_allow_user` – filter, return `false` to block a matched user (e.g. group checks).
* `m365_login_success` – action after a successful sign-in, receives the user and verified claims.
+* `m365_login_block_password_login` – filter, return `false` to exempt a password sign-in from button-only mode.
== External services ==
@@ -104,7 +105,7 @@ Open the fallback link shown on the Security tab (`wp-login.php?m365_fallback=KE
= Does it work with custom login pages? =
-Yes, use the shortcode `[m365_login_button redirect="/dashboard/"]`.
+Yes. Forms rendered with `wp_login_form()` get the button automatically. For page builders use the shortcode `[m365_login_button redirect="/dashboard/" divider="yes"]`, in theme templates call `m365_login_button()`. Enter the page's URL on the Button tab so error messages and the fallback link point there. Button-only mode blocks password sign-ins from custom forms as well.
= Does it support multisite? =